mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-11 09:27:07 +02:00
Compare commits
14 Commits
v3.8.2-pre
...
index-to-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
562a0a4285 | ||
|
|
8a483a7da0 | ||
|
|
cc47ff2b46 | ||
|
|
33b4e2e446 | ||
|
|
4185ad2419 | ||
|
|
b5a7af99f8 | ||
|
|
f830fc6490 | ||
|
|
1ff0ddacec | ||
|
|
8e73c88b68 | ||
|
|
d954986bce | ||
|
|
bffb101d5b | ||
|
|
b702d8dd22 | ||
|
|
d29741b20e | ||
|
|
76c218a220 |
14
.github/workflows/docker-hub.yml
vendored
14
.github/workflows/docker-hub.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
images: lasuite/impress-backend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
context: .
|
||||
target: backend-production
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
images: lasuite/impress-frontend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
build-args: |
|
||||
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
PUBLISH_AS_MIT=false
|
||||
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
images: lasuite/impress-y-provider
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
|
||||
if: github.event_name != 'pull_request'
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
-
|
||||
name: Run trivy scan
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
file: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
- build-and-push-frontend
|
||||
- build-and-push-backend
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: numerique-gouv/action-argocd-webhook-notification@main
|
||||
id: notify
|
||||
|
||||
27
.github/workflows/label_preview.yml
vendored
27
.github/workflows/label_preview.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Label Preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, opened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.pull_request.labels.*.name, 'preview')
|
||||
steps:
|
||||
- uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: |
|
||||
:rocket: Preview will be available at [https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/](https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/)
|
||||
|
||||
You can use the existing account with these credentials:
|
||||
- username: `docs`
|
||||
- password: `docs`
|
||||
|
||||
You can also create a new account if you want to.
|
||||
|
||||
Once this Pull Request is merged, the preview will be destroyed.
|
||||
comment-tag: preview-url
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -43,6 +43,10 @@ venv.bak/
|
||||
env.d/development/*.local
|
||||
env.d/terraform
|
||||
|
||||
# Docker
|
||||
compose.override.yml
|
||||
docker/auth/*.local
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
|
||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -6,42 +6,16 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.8.2] - 2025-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
🐛(service-worker) fix sw registration and page reload logic #1500
|
||||
|
||||
## [3.8.1] - 2025-10-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- ⚡️(backend) improve trashbin endpoint performance #1495
|
||||
- 🐛(backend) manage invitation partial update without email #1494
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿ add missing aria-label to add sub-doc button for accessibility #1480
|
||||
- ♿ add missing aria-label to more options button on sub-docs #1481
|
||||
|
||||
### Removed
|
||||
|
||||
- 🔥(backend) remove treebeard form for the document admin #1470
|
||||
|
||||
|
||||
## [3.8.0] - 2025-10-14
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add pdf block to the editor #1293
|
||||
- ✨List and restore deleted docs #1450
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(frontend) Refactor Auth component for improved redirection logic #1461
|
||||
- ♻️(frontend) replace Arial font-family with token font #1411
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿(frontend) enable enter key to open documentss #1354
|
||||
- ♿(frontend) improve modal a11y: structure, labels, title #1349
|
||||
- ♿improve NVDA navigation in DocShareModal #1396
|
||||
- #1354
|
||||
- #1349
|
||||
- ♿ improve accessibility by adding landmark roles to layout #1394
|
||||
- ♿ add document visible in list and openable via enter key #1365
|
||||
- ♿ add pdf outline property to enable bookmarks display #1368
|
||||
@@ -51,30 +25,25 @@ and this project adheres to
|
||||
- ♿ remove redundant aria-label on hidden icons and update tests #1432
|
||||
- ♿ improve semantic structure and aria roles of leftpanel #1431
|
||||
- ♿ add default background to left panel for better accessibility #1423
|
||||
- ♿ restyle checked checkboxes: removing strikethrough #1439
|
||||
- ♿ restyle checked checkboxes: removing strikethrough #1439
|
||||
- ♿ add h1 for SR on 40X pages and remove alt texts #1438
|
||||
- ♿ update labels and shared document icon accessibility #1442
|
||||
- 🍱(frontend) Fonts GDPR compliants #1453
|
||||
- ♻️(service-worker) improve SW registration and update handling #1473
|
||||
- ✨(backend) add async indexation of documents on save (or access save) #1276
|
||||
- ✨(backend) add debounce mechanism to limit indexation jobs #1276
|
||||
- ✨(api) add API route to search for indexed documents in Find #1276
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) duplicate sub docs as root for reader users #1385
|
||||
- 🐛(backend) duplicate sub docs as root for reader users
|
||||
- ⚗️(service-worker) remove index from cache first strategy #1395
|
||||
- 🐛(frontend) fix 404 page when reload 403 page #1402
|
||||
- 🐛(frontend) fix legacy role computation #1376
|
||||
- 🛂(frontend) block editing title when not allowed #1412
|
||||
- 🐛(frontend) scroll back to top when navigate to a document #1406
|
||||
- 🐛(frontend) fix export pdf emoji problem #1453
|
||||
- 🐛(frontend) fix attachment download filename #1447
|
||||
- 🐛(frontend) exclude h4-h6 headings from table of contents #1441
|
||||
- 🔒(frontend) prevent readers from changing callout emoji #1449
|
||||
- 🐛(frontend) fix overlapping placeholders in multi-column layout #1455
|
||||
- 🐛(backend) filter invitation with case insensitive email #1457
|
||||
- 🐛(frontend) reduce no access image size from 450 to 300 #1463
|
||||
- 🐛(frontend) preserve interlink style on drag-and-drop in editor #1460
|
||||
- ✨(frontend) load docs logo from public folder via url #1462
|
||||
- 🔧(keycloak) Fix https required issue in dev mode #1286
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿improve NVDA navigation in DocShareModal #1396
|
||||
|
||||
## [3.7.0] - 2025-09-12
|
||||
|
||||
@@ -138,6 +107,7 @@ and this project adheres to
|
||||
- 🐛(frontend) fix dnd conflict with tree and Blocknote #1328
|
||||
- 🐛(frontend) fix display bug on homepage #1332
|
||||
- 🐛link role update #1287
|
||||
- 🔧(keycloak) Fix https required issue in dev mode #1286
|
||||
|
||||
## [3.5.0] - 2025-07-31
|
||||
|
||||
@@ -809,10 +779,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.8.2...main
|
||||
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
|
||||
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
|
||||
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
|
||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.7.0...main
|
||||
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
|
||||
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
|
||||
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
|
||||
|
||||
@@ -94,14 +94,6 @@ RUN chmod g=u /etc/passwd
|
||||
# Copy installed python dependencies
|
||||
COPY --from=back-builder /install /usr/local
|
||||
|
||||
# Link certifi certificate from a static path /cert/cacert.pem to avoid issues
|
||||
# when python is upgraded and the path to the certificate changes.
|
||||
# The space between print and the ( is intended otherwise the git lint is failing
|
||||
RUN mkdir /cert && \
|
||||
path=`python -c 'import certifi;print (certifi.where())'` && \
|
||||
mv $path /cert/ && \
|
||||
ln -s /cert/cacert.pem $path
|
||||
|
||||
# Copy impress application (see .dockerignore)
|
||||
COPY ./src/backend /app/
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -247,6 +247,10 @@ demo: ## flush db then create a demo for load testing purpose
|
||||
@$(MANAGE) create_demo
|
||||
.PHONY: demo
|
||||
|
||||
index: ## index all documents to remote search
|
||||
@$(MANAGE) index
|
||||
.PHONY: index
|
||||
|
||||
# Nota bene: Black should come after isort just in case they don't agree...
|
||||
lint: ## lint back-end python sources
|
||||
lint: \
|
||||
|
||||
6
bin/fernetkey
Executable file
6
bin/fernetkey
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck source=bin/_config.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_dc_run app-dev python -c 'from cryptography.fernet import Fernet;import sys; sys.stdout.write("\n" + Fernet.generate_key().decode() + "\n");'
|
||||
18
compose.yml
18
compose.yml
@@ -72,6 +72,11 @@ services:
|
||||
- env.d/development/postgresql.local
|
||||
ports:
|
||||
- "8071:8000"
|
||||
networks:
|
||||
default: {}
|
||||
lasuite-net:
|
||||
aliases:
|
||||
- impress
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
@@ -92,6 +97,9 @@ services:
|
||||
command: ["celery", "-A", "impress.celery_app", "worker", "-l", "DEBUG"]
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
networks:
|
||||
- default
|
||||
- lasuite-net
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/common.local
|
||||
@@ -107,6 +115,11 @@ services:
|
||||
image: nginx:1.25
|
||||
ports:
|
||||
- "8083:8083"
|
||||
networks:
|
||||
default: {}
|
||||
lasuite-net:
|
||||
aliases:
|
||||
- nginx
|
||||
volumes:
|
||||
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
depends_on:
|
||||
@@ -217,3 +230,8 @@ services:
|
||||
kc_postgresql:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
|
||||
networks:
|
||||
lasuite-net:
|
||||
name: lasuite-net
|
||||
driver: bridge
|
||||
|
||||
@@ -82,7 +82,7 @@ backend:
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumeMounts:
|
||||
- name: certs
|
||||
mountPath: /cert/cacert.pem
|
||||
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
|
||||
subPath: cacert.pem
|
||||
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
|
||||
@@ -32,24 +32,6 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
|
||||
|
||||
----
|
||||
|
||||
# **Your logo** 📝
|
||||
|
||||
You can add your own logo in the header from the theme customization file.
|
||||
|
||||
### Settings 🔧
|
||||
|
||||
```shellscript
|
||||
THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||
```
|
||||
|
||||
### Example of JSON
|
||||
|
||||
You can activate it with the `header.logo` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||
|
||||
This configuration is optional. If not set, the default logo will be used.
|
||||
|
||||
----
|
||||
|
||||
# **Footer Configuration** 📝
|
||||
|
||||
The footer is configurable from the theme customization file.
|
||||
|
||||
@@ -49,6 +49,14 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
|
||||
# Store OIDC tokens in the session
|
||||
OIDC_STORE_ACCESS_TOKEN = True
|
||||
OIDC_STORE_REFRESH_TOKEN = True # Store the encrypted refresh token in the session.
|
||||
|
||||
# Must be a valid Fernet key (32 url-safe base64-encoded bytes)
|
||||
# To create one, use the bin/fernetkey command.
|
||||
# OIDC_STORE_REFRESH_TOKEN_KEY="your-32-byte-encryption-key=="
|
||||
|
||||
# AI
|
||||
AI_FEATURE_ENABLED=true
|
||||
AI_BASE_URL=https://openaiendpoint.com
|
||||
@@ -67,5 +75,8 @@ DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
|
||||
Y_PROVIDER_API_KEY=yprovider-api-key
|
||||
|
||||
# Theme customization
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
|
||||
# impress
|
||||
SEARCH_INDEXER_CLASS="core.services.search_indexers.FindDocumentIndexer"
|
||||
SEARCH_INDEXER_SECRET=find-api-key-for-docs-with-exactly-50-chars-length # Key generated by create_demo in Find app.
|
||||
SEARCH_INDEXER_URL="http://find:8000/api/v1.0/documents/index/"
|
||||
SEARCH_INDEXER_QUERY_URL="http://find:8000/api/v1.0/documents/search/"
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.contrib.auth import admin as auth_admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from treebeard.admin import TreeAdmin
|
||||
from treebeard.forms import movenodeform_factory
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -156,6 +157,7 @@ class DocumentAdmin(TreeAdmin):
|
||||
},
|
||||
),
|
||||
)
|
||||
form = movenodeform_factory(models.Document)
|
||||
inlines = (DocumentAccessInline,)
|
||||
list_display = (
|
||||
"id",
|
||||
|
||||
@@ -90,7 +90,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
nb_accesses_direct = serializers.IntegerField(read_only=True)
|
||||
user_role = serializers.SerializerMethodField(read_only=True)
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
deleted_at = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Document
|
||||
@@ -103,7 +102,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"is_favorite",
|
||||
@@ -126,7 +124,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"is_favorite",
|
||||
@@ -168,10 +165,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
request = self.context.get("request")
|
||||
return instance.get_role(request.user) if request else None
|
||||
|
||||
def get_deleted_at(self, instance):
|
||||
"""Return the deleted_at of the current document."""
|
||||
return instance.ancestors_deleted_at
|
||||
|
||||
|
||||
class DocumentLightSerializer(serializers.ModelSerializer):
|
||||
"""Minial document serializer for nesting in document accesses."""
|
||||
@@ -200,7 +193,6 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"content",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"is_favorite",
|
||||
@@ -224,7 +216,6 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"is_favorite",
|
||||
"link_role",
|
||||
@@ -749,9 +740,6 @@ class InvitationSerializer(serializers.ModelSerializer):
|
||||
if self.instance is None:
|
||||
attrs["issuer"] = user
|
||||
|
||||
if attrs.get("email"):
|
||||
attrs["email"] = attrs["email"].lower()
|
||||
|
||||
return attrs
|
||||
|
||||
def validate_role(self, role):
|
||||
@@ -889,3 +877,13 @@ class MoveDocumentSerializer(serializers.Serializer):
|
||||
choices=enums.MoveNodePositionChoices.choices,
|
||||
default=enums.MoveNodePositionChoices.LAST_CHILD,
|
||||
)
|
||||
|
||||
|
||||
class FindDocumentSerializer(serializers.Serializer):
|
||||
"""Serializer for Find search requests"""
|
||||
|
||||
q = serializers.CharField(required=True, allow_blank=False, trim_whitespace=True)
|
||||
page_size = serializers.IntegerField(
|
||||
required=False, min_value=1, max_value=50, default=20
|
||||
)
|
||||
page = serializers.IntegerField(required=False, min_value=1, default=1)
|
||||
|
||||
@@ -21,6 +21,7 @@ from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Left, Length
|
||||
from django.http import Http404, StreamingHttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.text import capfirst, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -31,6 +32,7 @@ from botocore.exceptions import ClientError
|
||||
from csp.constants import NONE
|
||||
from csp.decorators import csp_update
|
||||
from lasuite.malware_detection import malware_detection
|
||||
from lasuite.oidc_login.decorators import refresh_oidc_access_token
|
||||
from rest_framework import filters, status, viewsets
|
||||
from rest_framework import response as drf_response
|
||||
from rest_framework.permissions import AllowAny
|
||||
@@ -47,6 +49,10 @@ from core.services.converter_services import (
|
||||
from core.services.converter_services import (
|
||||
YdocConverter,
|
||||
)
|
||||
from core.services.search_indexers import (
|
||||
get_document_indexer,
|
||||
get_visited_document_ids_of,
|
||||
)
|
||||
from core.tasks.mail import send_ask_for_access_mail
|
||||
from core.utils import extract_attachments, filter_descendants
|
||||
|
||||
@@ -373,6 +379,7 @@ class DocumentViewSet(
|
||||
list_serializer_class = serializers.ListDocumentSerializer
|
||||
trashbin_serializer_class = serializers.ListDocumentSerializer
|
||||
tree_serializer_class = serializers.ListDocumentSerializer
|
||||
search_serializer_class = serializers.ListDocumentSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get queryset performing all annotation and filtering on the document tree structure."""
|
||||
@@ -623,29 +630,12 @@ class DocumentViewSet(
|
||||
The selected documents are those deleted within the cutoff period defined in the
|
||||
settings (see TRASHBIN_CUTOFF_DAYS), before they are considered permanently deleted.
|
||||
"""
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return self.get_response_for_queryset(self.queryset.none())
|
||||
|
||||
access_documents_paths = (
|
||||
models.DocumentAccess.objects.select_related("document")
|
||||
.filter(
|
||||
db.Q(user=self.request.user) | db.Q(team__in=self.request.user.teams),
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
.values_list("document__path", flat=True)
|
||||
)
|
||||
|
||||
children_clause = db.Q()
|
||||
for path in access_documents_paths:
|
||||
children_clause |= db.Q(path__startswith=path)
|
||||
|
||||
queryset = self.queryset.filter(
|
||||
children_clause,
|
||||
deleted_at__isnull=False,
|
||||
deleted_at__gte=models.get_trashbin_cutoff(),
|
||||
)
|
||||
queryset = queryset.annotate_user_roles(self.request.user)
|
||||
queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER])
|
||||
|
||||
return self.get_response_for_queryset(queryset)
|
||||
|
||||
@@ -868,47 +858,33 @@ class DocumentViewSet(
|
||||
|
||||
try:
|
||||
current_document = (
|
||||
self.queryset.select_related(None)
|
||||
.only("depth", "path", "ancestors_deleted_at")
|
||||
.get(pk=pk)
|
||||
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
|
||||
)
|
||||
except models.Document.DoesNotExist as excpt:
|
||||
raise drf.exceptions.NotFound() from excpt
|
||||
|
||||
is_deleted = current_document.ancestors_deleted_at is not None
|
||||
|
||||
if is_deleted:
|
||||
if current_document.get_role(user) != models.RoleChoices.OWNER:
|
||||
raise (
|
||||
drf.exceptions.PermissionDenied()
|
||||
if request.user.is_authenticated
|
||||
else drf.exceptions.NotAuthenticated()
|
||||
)
|
||||
highest_readable = current_document
|
||||
ancestors = self.queryset.select_related(None).filter(pk=pk)
|
||||
else:
|
||||
ancestors = (
|
||||
(
|
||||
current_document.get_ancestors()
|
||||
| self.queryset.select_related(None).filter(pk=pk)
|
||||
)
|
||||
.filter(ancestors_deleted_at__isnull=True)
|
||||
.order_by("path")
|
||||
)
|
||||
# Get the highest readable ancestor
|
||||
highest_readable = (
|
||||
ancestors.select_related(None)
|
||||
.readable_per_se(request.user)
|
||||
.only("depth", "path")
|
||||
.first()
|
||||
ancestors = (
|
||||
(
|
||||
current_document.get_ancestors()
|
||||
| self.queryset.select_related(None).filter(pk=pk)
|
||||
)
|
||||
.filter(ancestors_deleted_at__isnull=True)
|
||||
.order_by("path")
|
||||
)
|
||||
|
||||
if highest_readable is None:
|
||||
raise (
|
||||
drf.exceptions.PermissionDenied()
|
||||
if request.user.is_authenticated
|
||||
else drf.exceptions.NotAuthenticated()
|
||||
)
|
||||
# Get the highest readable ancestor
|
||||
highest_readable = (
|
||||
ancestors.select_related(None)
|
||||
.readable_per_se(request.user)
|
||||
.only("depth", "path")
|
||||
.first()
|
||||
)
|
||||
if highest_readable is None:
|
||||
raise (
|
||||
drf.exceptions.PermissionDenied()
|
||||
if request.user.is_authenticated
|
||||
else drf.exceptions.NotAuthenticated()
|
||||
)
|
||||
paths_links_mapping = {}
|
||||
ancestors_links = []
|
||||
children_clause = db.Q()
|
||||
@@ -1061,6 +1037,68 @@ class DocumentViewSet(
|
||||
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
@drf.decorators.action(detail=False, methods=["get"], url_path="search")
|
||||
@method_decorator(refresh_oidc_access_token)
|
||||
def search(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns a DRF response containing the filtered, annotated and ordered document list.
|
||||
|
||||
Applies filtering based on request parameter 'q' from `FindDocumentSerializer`.
|
||||
Depending of the configuration it can be:
|
||||
- A fulltext search through the opensearch indexation app "find" if the backend is
|
||||
enabled (see SEARCH_BACKEND_CLASS)
|
||||
- A filtering by the model field 'title'.
|
||||
|
||||
The ordering is always by the most recent first.
|
||||
"""
|
||||
access_token = request.session.get("oidc_access_token")
|
||||
user = request.user
|
||||
|
||||
serializer = serializers.FindDocumentSerializer(data=request.query_params)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
indexer = get_document_indexer()
|
||||
text = serializer.validated_data["q"]
|
||||
|
||||
# The indexer is not configured, so we fallback on a simple filter on the
|
||||
# model field 'title'.
|
||||
if not indexer:
|
||||
# As the 'list' view we get a prefiltered queryset (deleted docs are excluded)
|
||||
queryset = self.get_queryset()
|
||||
filterset = DocumentFilter({"title": text}, queryset=queryset)
|
||||
|
||||
if not filterset.is_valid():
|
||||
raise drf.exceptions.ValidationError(filterset.errors)
|
||||
|
||||
queryset = filterset.filter_queryset(queryset).order_by("-updated_at")
|
||||
|
||||
return self.get_response_for_queryset(
|
||||
queryset,
|
||||
context={
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
|
||||
queryset = models.Document.objects.all()
|
||||
|
||||
# Retrieve the documents ids from Find.
|
||||
results = indexer.search(
|
||||
text=text,
|
||||
token=access_token,
|
||||
visited=get_visited_document_ids_of(queryset, user),
|
||||
page=serializer.validated_data.get("page", 1),
|
||||
page_size=serializer.validated_data.get("page_size", 20),
|
||||
)
|
||||
|
||||
queryset = queryset.filter(pk__in=results).order_by("-updated_at")
|
||||
|
||||
return self.get_response_for_queryset(
|
||||
queryset,
|
||||
context={
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
|
||||
@drf.decorators.action(detail=True, methods=["get"], url_path="versions")
|
||||
def versions_list(self, request, *args, **kwargs):
|
||||
"""
|
||||
@@ -2188,7 +2226,6 @@ class ConfigView(drf.views.APIView):
|
||||
"LANGUAGES",
|
||||
"LANGUAGE_CODE",
|
||||
"SENTRY_DSN",
|
||||
"TRASHBIN_CUTOFF_DAYS",
|
||||
]
|
||||
dict_settings = {}
|
||||
for setting in array_settings:
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
"""Impress Core application"""
|
||||
# from django.apps import AppConfig
|
||||
# from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
# class CoreConfig(AppConfig):
|
||||
# """Configuration class for the impress core app."""
|
||||
class CoreConfig(AppConfig):
|
||||
"""Configuration class for the impress core app."""
|
||||
|
||||
# name = "core"
|
||||
# app_label = "core"
|
||||
# verbose_name = _("impress core application")
|
||||
name = "core"
|
||||
app_label = "core"
|
||||
verbose_name = _("Impress core application")
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Import signals when the app is ready.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel, unused-import
|
||||
from . import signals # noqa: PLC0415
|
||||
|
||||
40
src/backend/core/management/commands/index.py
Normal file
40
src/backend/core/management/commands/index.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Handle search setup that needs to be done at bootstrap time.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from core.services.search_indexers import get_document_indexer
|
||||
|
||||
logger = logging.getLogger("docs.search.bootstrap_search")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Index all documents to remote search service"""
|
||||
|
||||
help = __doc__
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Launch and log search index generation."""
|
||||
indexer = get_document_indexer()
|
||||
|
||||
if not indexer:
|
||||
raise CommandError("The indexer is not enabled or properly configured.")
|
||||
|
||||
logger.info("Starting to regenerate Find index...")
|
||||
start = time.perf_counter()
|
||||
|
||||
try:
|
||||
count = indexer.index()
|
||||
except Exception as err:
|
||||
raise CommandError("Unable to regenerate index") from err
|
||||
|
||||
duration = time.perf_counter() - start
|
||||
logger.info(
|
||||
"Search index regenerated from %d document(s) in %.2f seconds.",
|
||||
count,
|
||||
duration,
|
||||
)
|
||||
@@ -221,7 +221,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
Expired invitations are ignored.
|
||||
"""
|
||||
valid_invitations = Invitation.objects.filter(
|
||||
email__iexact=self.email,
|
||||
email=self.email,
|
||||
created_at__gte=(
|
||||
timezone.now()
|
||||
- timedelta(seconds=settings.INVITATION_VALIDITY_DURATION)
|
||||
@@ -430,32 +430,35 @@ class Document(MP_Node, BaseModel):
|
||||
def save(self, *args, **kwargs):
|
||||
"""Write content to object storage only if _content has changed."""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self._content:
|
||||
file_key = self.file_key
|
||||
bytes_content = self._content.encode("utf-8")
|
||||
self.save_content(self._content)
|
||||
|
||||
# Attempt to directly check if the object exists using the storage client.
|
||||
try:
|
||||
response = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=file_key
|
||||
)
|
||||
except ClientError as excpt:
|
||||
# If the error is a 404, the object doesn't exist, so we should create it.
|
||||
if excpt.response["Error"]["Code"] == "404":
|
||||
has_changed = True
|
||||
else:
|
||||
raise
|
||||
def save_content(self, content):
|
||||
"""Save content to object storage."""
|
||||
|
||||
file_key = self.file_key
|
||||
bytes_content = content.encode("utf-8")
|
||||
|
||||
# Attempt to directly check if the object exists using the storage client.
|
||||
try:
|
||||
response = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=file_key
|
||||
)
|
||||
except ClientError as excpt:
|
||||
# If the error is a 404, the object doesn't exist, so we should create it.
|
||||
if excpt.response["Error"]["Code"] == "404":
|
||||
has_changed = True
|
||||
else:
|
||||
# Compare the existing ETag with the MD5 hash of the new content.
|
||||
has_changed = (
|
||||
response["ETag"].strip('"')
|
||||
!= hashlib.md5(bytes_content).hexdigest() # noqa: S324
|
||||
)
|
||||
raise
|
||||
else:
|
||||
# Compare the existing ETag with the MD5 hash of the new content.
|
||||
has_changed = (
|
||||
response["ETag"].strip('"') != hashlib.md5(bytes_content).hexdigest() # noqa: S324
|
||||
)
|
||||
|
||||
if has_changed:
|
||||
content_file = ContentFile(bytes_content)
|
||||
default_storage.save(file_key, content_file)
|
||||
if has_changed:
|
||||
content_file = ContentFile(bytes_content)
|
||||
default_storage.save(file_key, content_file)
|
||||
|
||||
def is_leaf(self):
|
||||
"""
|
||||
@@ -721,7 +724,7 @@ class Document(MP_Node, BaseModel):
|
||||
|
||||
# Characteristics that are based only on specific access
|
||||
is_owner = role == RoleChoices.OWNER
|
||||
is_deleted = self.ancestors_deleted_at
|
||||
is_deleted = self.ancestors_deleted_at and not is_owner
|
||||
is_owner_or_admin = (is_owner or role == RoleChoices.ADMIN) and not is_deleted
|
||||
|
||||
# Compute access roles before adding link roles because we don't
|
||||
@@ -750,7 +753,6 @@ class Document(MP_Node, BaseModel):
|
||||
role = RoleChoices.max(role, link_definition["link_role"])
|
||||
|
||||
can_get = bool(role) and not is_deleted
|
||||
retrieve = can_get or is_owner
|
||||
can_update = (
|
||||
is_owner_or_admin or role == RoleChoices.EDITOR
|
||||
) and not is_deleted
|
||||
@@ -759,7 +761,7 @@ class Document(MP_Node, BaseModel):
|
||||
is_owner
|
||||
if self.is_root()
|
||||
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
|
||||
) and not is_deleted
|
||||
)
|
||||
|
||||
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
|
||||
ai_access = any(
|
||||
@@ -791,15 +793,15 @@ class Document(MP_Node, BaseModel):
|
||||
"duplicate": can_get and user.is_authenticated,
|
||||
"favorite": can_get and user.is_authenticated,
|
||||
"link_configuration": is_owner_or_admin,
|
||||
"invite_owner": is_owner and not is_deleted,
|
||||
"invite_owner": is_owner,
|
||||
"mask": can_get and user.is_authenticated,
|
||||
"move": is_owner_or_admin and not is_deleted,
|
||||
"move": is_owner_or_admin and not self.ancestors_deleted_at,
|
||||
"partial_update": can_update,
|
||||
"restore": is_owner,
|
||||
"retrieve": retrieve,
|
||||
"retrieve": can_get,
|
||||
"media_auth": can_get,
|
||||
"link_select_options": link_select_options,
|
||||
"tree": retrieve,
|
||||
"tree": can_get,
|
||||
"update": can_update,
|
||||
"versions_destroy": is_owner_or_admin,
|
||||
"versions_list": has_access_role,
|
||||
|
||||
303
src/backend/core/services/search_indexers.py
Normal file
303
src/backend/core/services/search_indexers.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""Document search index management utilities and indexers"""
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from functools import cache
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import Subquery
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
import requests
|
||||
|
||||
from core import models, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@cache
|
||||
def get_document_indexer():
|
||||
"""Returns an instance of indexer service if enabled and properly configured."""
|
||||
classpath = settings.SEARCH_INDEXER_CLASS
|
||||
|
||||
# For this usecase an empty indexer class is not an issue but a feature.
|
||||
if not classpath:
|
||||
logger.info("Document indexer is not configured (see SEARCH_INDEXER_CLASS)")
|
||||
return None
|
||||
|
||||
try:
|
||||
indexer_class = import_string(settings.SEARCH_INDEXER_CLASS)
|
||||
return indexer_class()
|
||||
except ImportError as err:
|
||||
logger.error("SEARCH_INDEXER_CLASS setting is not valid : %s", err)
|
||||
except ImproperlyConfigured as err:
|
||||
logger.error("Document indexer is not properly configured : %s", err)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_batch_accesses_by_users_and_teams(paths):
|
||||
"""
|
||||
Get accesses related to a list of document paths,
|
||||
grouped by users and teams, including all ancestor paths.
|
||||
"""
|
||||
ancestor_map = utils.get_ancestor_to_descendants_map(
|
||||
paths, steplen=models.Document.steplen
|
||||
)
|
||||
ancestor_paths = list(ancestor_map.keys())
|
||||
|
||||
access_qs = models.DocumentAccess.objects.filter(
|
||||
document__path__in=ancestor_paths
|
||||
).values("document__path", "user__sub", "team")
|
||||
|
||||
access_by_document_path = defaultdict(lambda: {"users": set(), "teams": set()})
|
||||
|
||||
for access in access_qs:
|
||||
ancestor_path = access["document__path"]
|
||||
user_sub = access["user__sub"]
|
||||
team = access["team"]
|
||||
|
||||
for descendant_path in ancestor_map.get(ancestor_path, []):
|
||||
if user_sub:
|
||||
access_by_document_path[descendant_path]["users"].add(str(user_sub))
|
||||
if team:
|
||||
access_by_document_path[descendant_path]["teams"].add(team)
|
||||
|
||||
return dict(access_by_document_path)
|
||||
|
||||
|
||||
def get_visited_document_ids_of(queryset, user):
|
||||
"""
|
||||
Returns the ids of the documents that have a linktrace to the user and NOT owned.
|
||||
It will be use to limit the opensearch responses to the public documents already
|
||||
"visited" by the user.
|
||||
"""
|
||||
if isinstance(user, AnonymousUser):
|
||||
return []
|
||||
|
||||
qs = models.LinkTrace.objects.filter(user=user)
|
||||
|
||||
docs = (
|
||||
queryset.exclude(accesses__user=user)
|
||||
.filter(
|
||||
deleted_at__isnull=True,
|
||||
ancestors_deleted_at__isnull=True,
|
||||
)
|
||||
.filter(pk__in=Subquery(qs.values("document_id")))
|
||||
.order_by("pk")
|
||||
.distinct("pk")
|
||||
)
|
||||
|
||||
return [str(id) for id in docs.values_list("pk", flat=True)]
|
||||
|
||||
|
||||
class BaseDocumentIndexer(ABC):
|
||||
"""
|
||||
Base class for document indexers.
|
||||
|
||||
Handles batching and access resolution. Subclasses must implement both
|
||||
`serialize_document()` and `push()` to define backend-specific behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, batch_size=None):
|
||||
"""
|
||||
Initialize the indexer.
|
||||
|
||||
Args:
|
||||
batch_size (int, optional): Number of documents per batch.
|
||||
Defaults to settings.SEARCH_INDEXER_BATCH_SIZE.
|
||||
"""
|
||||
self.batch_size = batch_size or settings.SEARCH_INDEXER_BATCH_SIZE
|
||||
self.indexer_url = settings.SEARCH_INDEXER_URL
|
||||
self.indexer_secret = settings.SEARCH_INDEXER_SECRET
|
||||
self.search_url = settings.SEARCH_INDEXER_QUERY_URL
|
||||
|
||||
if not self.indexer_url:
|
||||
raise ImproperlyConfigured(
|
||||
"SEARCH_INDEXER_URL must be set in Django settings."
|
||||
)
|
||||
|
||||
if not self.indexer_secret:
|
||||
raise ImproperlyConfigured(
|
||||
"SEARCH_INDEXER_SECRET must be set in Django settings."
|
||||
)
|
||||
|
||||
if not self.search_url:
|
||||
raise ImproperlyConfigured(
|
||||
"SEARCH_INDEXER_QUERY_URL must be set in Django settings."
|
||||
)
|
||||
|
||||
def index(self):
|
||||
"""
|
||||
Fetch documents in batches, serialize them, and push to the search backend.
|
||||
"""
|
||||
last_id = 0
|
||||
count = 0
|
||||
|
||||
while True:
|
||||
documents_batch = list(
|
||||
models.Document.objects.filter(
|
||||
id__gt=last_id,
|
||||
).order_by("id")[: self.batch_size]
|
||||
)
|
||||
|
||||
if not documents_batch:
|
||||
break
|
||||
|
||||
doc_paths = [doc.path for doc in documents_batch]
|
||||
last_id = documents_batch[-1].id
|
||||
accesses_by_document_path = get_batch_accesses_by_users_and_teams(doc_paths)
|
||||
|
||||
serialized_batch = [
|
||||
self.serialize_document(document, accesses_by_document_path)
|
||||
for document in documents_batch
|
||||
if document.content or document.title
|
||||
]
|
||||
|
||||
self.push(serialized_batch)
|
||||
count += len(serialized_batch)
|
||||
|
||||
return count
|
||||
|
||||
@abstractmethod
|
||||
def serialize_document(self, document, accesses):
|
||||
"""
|
||||
Convert a Document instance to a JSON-serializable format for indexing.
|
||||
|
||||
Must be implemented by subclasses.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def push(self, data):
|
||||
"""
|
||||
Push a batch of serialized documents to the backend.
|
||||
|
||||
Must be implemented by subclasses.
|
||||
"""
|
||||
|
||||
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
|
||||
def search(self, text, token, visited=(), page=1, page_size=50):
|
||||
"""
|
||||
Search for documents in Find app.
|
||||
Ensure the same default ordering as "Docs" list : -updated_at
|
||||
|
||||
Returns ids of the documents
|
||||
|
||||
Args:
|
||||
text (str): Text search content.
|
||||
token (str): OIDC Authentication token.
|
||||
visited (list, optional):
|
||||
List of ids of active public documents with LinkTrace
|
||||
Defaults to settings.SEARCH_INDEXER_BATCH_SIZE.
|
||||
page (int, optional):
|
||||
The page number to retrieve.
|
||||
Defaults to 1 if not specified.
|
||||
page_size (int, optional):
|
||||
The number of results to return per page.
|
||||
Defaults to 50 if not specified.
|
||||
"""
|
||||
response = self.search_query(
|
||||
data={
|
||||
"q": text,
|
||||
"visited": visited,
|
||||
"services": ["docs"],
|
||||
"page_number": page,
|
||||
"page_size": page_size,
|
||||
"order_by": "updated_at",
|
||||
"order_direction": "desc",
|
||||
},
|
||||
token=token,
|
||||
)
|
||||
|
||||
return [d["_id"] for d in response]
|
||||
|
||||
@abstractmethod
|
||||
def search_query(self, data, token) -> dict:
|
||||
"""
|
||||
Retrieve documents from the Find app API.
|
||||
|
||||
Must be implemented by subclasses.
|
||||
"""
|
||||
|
||||
|
||||
class FindDocumentIndexer(BaseDocumentIndexer):
|
||||
"""
|
||||
Document indexer that pushes documents to La Suite Find app.
|
||||
"""
|
||||
|
||||
def serialize_document(self, document, accesses):
|
||||
"""
|
||||
Convert a Document to the JSON format expected by La Suite Find.
|
||||
|
||||
Args:
|
||||
document (Document): The document instance.
|
||||
accesses (dict): Mapping of document ID to user/team access.
|
||||
|
||||
Returns:
|
||||
dict: A JSON-serializable dictionary.
|
||||
"""
|
||||
doc_path = document.path
|
||||
doc_content = document.content
|
||||
text_content = utils.base64_yjs_to_text(doc_content) if doc_content else ""
|
||||
|
||||
return {
|
||||
"id": str(document.id),
|
||||
"title": document.title or "",
|
||||
"content": text_content,
|
||||
"depth": document.depth,
|
||||
"path": document.path,
|
||||
"numchild": document.numchild,
|
||||
"created_at": document.created_at.isoformat(),
|
||||
"updated_at": document.updated_at.isoformat(),
|
||||
"users": list(accesses.get(doc_path, {}).get("users", set())),
|
||||
"groups": list(accesses.get(doc_path, {}).get("teams", set())),
|
||||
"reach": document.computed_link_reach,
|
||||
"size": len(text_content.encode("utf-8")),
|
||||
"is_active": not bool(document.ancestors_deleted_at),
|
||||
}
|
||||
|
||||
def search_query(self, data, token) -> requests.Response:
|
||||
"""
|
||||
Retrieve documents from the Find app API.
|
||||
|
||||
Args:
|
||||
data (dict): search data
|
||||
token (str): OICD token
|
||||
|
||||
Returns:
|
||||
dict: A JSON-serializable dictionary.
|
||||
"""
|
||||
try:
|
||||
response = requests.post(
|
||||
self.search_url,
|
||||
json=data,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.error("HTTPError: %s", e)
|
||||
raise
|
||||
|
||||
def push(self, data):
|
||||
"""
|
||||
Push a batch of documents to the Find backend.
|
||||
|
||||
Args:
|
||||
data (list): List of document dictionaries.
|
||||
"""
|
||||
try:
|
||||
response = requests.post(
|
||||
self.indexer_url,
|
||||
json=data,
|
||||
headers={"Authorization": f"Bearer {self.indexer_secret}"},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.error("HTTPError: %s", e)
|
||||
raise
|
||||
31
src/backend/core/signals.py
Normal file
31
src/backend/core/signals.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Declare and configure the signals for the impress core application
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import signals
|
||||
from django.dispatch import receiver
|
||||
|
||||
from . import models
|
||||
from .tasks.find import trigger_document_indexer
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender=models.Document)
|
||||
def document_post_save(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Asynchronous call to the document indexer at the end of the transaction.
|
||||
Note : Within the transaction we can have an empty content and a serialization
|
||||
error.
|
||||
"""
|
||||
transaction.on_commit(partial(trigger_document_indexer, instance))
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender=models.DocumentAccess)
|
||||
def document_access_post_save(sender, instance, created, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Asynchronous call to the document indexer at the end of the transaction.
|
||||
"""
|
||||
if not created:
|
||||
transaction.on_commit(partial(trigger_document_indexer, instance.document))
|
||||
89
src/backend/core/tasks/find.py
Normal file
89
src/backend/core/tasks/find.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Trigger document indexation using celery task."""
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from impress.celery_app import app
|
||||
|
||||
logger = getLogger(__file__)
|
||||
|
||||
|
||||
def indexer_debounce_lock(document_id):
|
||||
"""Increase or reset counter"""
|
||||
key = f"doc-indexer-debounce-{document_id}"
|
||||
|
||||
try:
|
||||
return cache.incr(key)
|
||||
except ValueError:
|
||||
cache.set(key, 1)
|
||||
return 1
|
||||
|
||||
|
||||
def indexer_debounce_release(document_id):
|
||||
"""Decrease or reset counter"""
|
||||
key = f"doc-indexer-debounce-{document_id}"
|
||||
|
||||
try:
|
||||
return cache.decr(key)
|
||||
except ValueError:
|
||||
cache.set(key, 0)
|
||||
return 0
|
||||
|
||||
|
||||
@app.task
|
||||
def document_indexer_task(document_id):
|
||||
"""Celery Task : Sends indexation query for a document."""
|
||||
# Prevents some circular imports
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from core import models # noqa : PLC0415
|
||||
from core.services.search_indexers import ( # noqa : PLC0415
|
||||
get_batch_accesses_by_users_and_teams,
|
||||
get_document_indexer,
|
||||
)
|
||||
|
||||
# check if the counter : if still up, skip the task. only the last one
|
||||
# within the countdown delay will do the query.
|
||||
if indexer_debounce_release(document_id) > 0:
|
||||
logger.info("Skip document %s indexation", document_id)
|
||||
return
|
||||
|
||||
indexer = get_document_indexer()
|
||||
|
||||
if indexer is None:
|
||||
return
|
||||
|
||||
doc = models.Document.objects.get(pk=document_id)
|
||||
accesses = get_batch_accesses_by_users_and_teams((doc.path,))
|
||||
|
||||
data = indexer.serialize_document(document=doc, accesses=accesses)
|
||||
|
||||
logger.info("Start document %s indexation", document_id)
|
||||
indexer.push(data)
|
||||
|
||||
|
||||
def trigger_document_indexer(document):
|
||||
"""
|
||||
Trigger indexation task with debounce a delay set by the SEARCH_INDEXER_COUNTDOWN setting.
|
||||
|
||||
Args:
|
||||
document (Document): The document instance.
|
||||
"""
|
||||
countdown = settings.SEARCH_INDEXER_COUNTDOWN
|
||||
|
||||
# DO NOT create a task if indexation if disabled
|
||||
if not settings.SEARCH_INDEXER_CLASS:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"Add task for document %s indexation in %.2f seconds",
|
||||
document.pk,
|
||||
countdown,
|
||||
)
|
||||
|
||||
# Each time this method is called during the countdown, we increment the
|
||||
# counter and each task decrease it, so the index be run only once.
|
||||
indexer_debounce_lock(document.pk)
|
||||
|
||||
document_indexer_task.apply_async(args=[document.pk], countdown=countdown)
|
||||
65
src/backend/core/tests/commands/test_index.py
Normal file
65
src/backend/core/tests/commands/test_index.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Unit test for `index` command.
|
||||
"""
|
||||
|
||||
from operator import itemgetter
|
||||
from unittest import mock
|
||||
|
||||
from django.core.management import CommandError, call_command
|
||||
from django.db import transaction
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories
|
||||
from core.services.search_indexers import FindDocumentIndexer
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_index():
|
||||
"""Test the command `index` that run the Find app indexer for all the available documents."""
|
||||
user = factories.UserFactory()
|
||||
indexer = FindDocumentIndexer()
|
||||
|
||||
with transaction.atomic():
|
||||
doc = factories.DocumentFactory()
|
||||
empty_doc = factories.DocumentFactory(title=None, content="")
|
||||
no_title_doc = factories.DocumentFactory(title=None)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=doc, user=user)
|
||||
factories.UserDocumentAccessFactory(document=empty_doc, user=user)
|
||||
factories.UserDocumentAccessFactory(document=no_title_doc, user=user)
|
||||
|
||||
accesses = {
|
||||
str(doc.path): {"users": [user.sub]},
|
||||
str(empty_doc.path): {"users": [user.sub]},
|
||||
str(no_title_doc.path): {"users": [user.sub]},
|
||||
}
|
||||
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
call_command("index")
|
||||
|
||||
push_call_args = [call.args[0] for call in mock_push.call_args_list]
|
||||
|
||||
# called once but with a batch of docs
|
||||
mock_push.assert_called_once()
|
||||
|
||||
assert sorted(push_call_args[0], key=itemgetter("id")) == sorted(
|
||||
[
|
||||
indexer.serialize_document(doc, accesses),
|
||||
indexer.serialize_document(no_title_doc, accesses),
|
||||
],
|
||||
key=itemgetter("id"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_index_improperly_configured(indexer_settings):
|
||||
"""The command should raise an exception if the indexer is not configured"""
|
||||
indexer_settings.SEARCH_INDEXER_CLASS = None
|
||||
|
||||
with pytest.raises(CommandError) as err:
|
||||
call_command("index")
|
||||
|
||||
assert str(err.value) == "The indexer is not enabled or properly configured."
|
||||
@@ -24,3 +24,29 @@ def mock_user_teams():
|
||||
"core.models.User.teams", new_callable=mock.PropertyMock
|
||||
) as mock_teams:
|
||||
yield mock_teams
|
||||
|
||||
|
||||
@pytest.fixture(name="indexer_settings")
|
||||
def indexer_settings_fixture(settings):
|
||||
"""
|
||||
Setup valid settings for the document indexer. Clear the indexer cache.
|
||||
"""
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from core.services.search_indexers import ( # noqa: PLC0415
|
||||
get_document_indexer,
|
||||
)
|
||||
|
||||
get_document_indexer.cache_clear()
|
||||
|
||||
settings.SEARCH_INDEXER_CLASS = "core.services.search_indexers.FindDocumentIndexer"
|
||||
settings.SEARCH_INDEXER_SECRET = "ThisIsAKeyForTest"
|
||||
settings.SEARCH_INDEXER_URL = "http://localhost:8081/api/v1.0/documents/index/"
|
||||
settings.SEARCH_INDEXER_QUERY_URL = (
|
||||
"http://localhost:8081/api/v1.0/documents/search/"
|
||||
)
|
||||
|
||||
yield settings
|
||||
|
||||
# clear cache to prevent issues with other tests
|
||||
get_document_indexer.cache_clear()
|
||||
|
||||
@@ -596,32 +596,6 @@ def test_api_document_invitations_create_cannot_invite_existing_users():
|
||||
}
|
||||
|
||||
|
||||
def test_api_document_invitations_create_lower_email():
|
||||
"""
|
||||
No matter the case, the email should be converted to lowercase.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
|
||||
# Build an invitation to the email of an existing identity in the db
|
||||
invitation_values = {
|
||||
"email": "GuEst@example.com",
|
||||
"role": random.choice(models.RoleChoices.values),
|
||||
}
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["email"] == "guest@example.com"
|
||||
|
||||
|
||||
# Update
|
||||
|
||||
|
||||
@@ -769,37 +743,6 @@ def test_api_document_invitations_update_authenticated_unprivileged(
|
||||
assert value == old_invitation_values[key]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("role", ["administrator", "owner"])
|
||||
def test_api_document_invitations_patch(via, role, mock_user_teams):
|
||||
"""Partially updating an invitation should be allowed."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
invitation = factories.InvitationFactory(role="editor")
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=invitation.document, user=user, role=role
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=invitation.document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{invitation.document.id!s}/invitations/{invitation.id!s}/",
|
||||
{"role": "reader"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
invitation.refresh_from_db()
|
||||
assert invitation.role == "reader"
|
||||
|
||||
|
||||
# Delete
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ def test_api_documents_children_list_anonymous_public_standalone(
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -64,7 +63,6 @@ def test_api_documents_children_list_anonymous_public_standalone(
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -117,7 +115,6 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -140,7 +137,6 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -212,7 +208,6 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -235,7 +230,6 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -293,7 +287,6 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -316,7 +309,6 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -401,7 +393,6 @@ def test_api_documents_children_list_authenticated_related_direct(
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -424,7 +415,6 @@ def test_api_documents_children_list_authenticated_related_direct(
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -485,7 +475,6 @@ def test_api_documents_children_list_authenticated_related_parent(
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -508,7 +497,6 @@ def test_api_documents_children_list_authenticated_related_parent(
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -621,7 +609,6 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -644,7 +631,6 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
|
||||
@@ -38,7 +38,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -63,7 +62,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 3,
|
||||
"excerpt": grand_child.excerpt,
|
||||
"id": str(grand_child.id),
|
||||
@@ -86,7 +84,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -138,7 +135,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -161,7 +157,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 5,
|
||||
"excerpt": grand_child.excerpt,
|
||||
"id": str(grand_child.id),
|
||||
@@ -184,7 +179,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -257,7 +251,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -280,7 +273,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 3,
|
||||
"excerpt": grand_child.excerpt,
|
||||
"id": str(grand_child.id),
|
||||
@@ -303,7 +295,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -361,7 +352,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -384,7 +374,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 5,
|
||||
"excerpt": grand_child.excerpt,
|
||||
"id": str(grand_child.id),
|
||||
@@ -407,7 +396,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -486,7 +474,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -509,7 +496,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 3,
|
||||
"excerpt": grand_child.excerpt,
|
||||
"id": str(grand_child.id),
|
||||
@@ -532,7 +518,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -591,7 +576,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -614,7 +598,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 5,
|
||||
"excerpt": grand_child.excerpt,
|
||||
"id": str(grand_child.id),
|
||||
@@ -637,7 +620,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 4,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
@@ -742,7 +724,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child1.excerpt,
|
||||
"id": str(child1.id),
|
||||
@@ -765,7 +746,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"computed_link_role": grand_child.computed_link_role,
|
||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_child.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 3,
|
||||
"excerpt": grand_child.excerpt,
|
||||
"id": str(grand_child.id),
|
||||
@@ -788,7 +768,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 2,
|
||||
"excerpt": child2.excerpt,
|
||||
"id": str(child2.id),
|
||||
|
||||
@@ -65,7 +65,6 @@ def test_api_document_favorite_list_authenticated_with_favorite():
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"content": document.content,
|
||||
"depth": document.depth,
|
||||
"excerpt": document.excerpt,
|
||||
|
||||
@@ -69,7 +69,6 @@ def test_api_documents_list_format():
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": True,
|
||||
|
||||
@@ -70,7 +70,6 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
@@ -145,7 +144,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 3,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
@@ -254,7 +252,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
"link_reach": reach,
|
||||
@@ -336,7 +333,6 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
"link_reach": document.link_reach,
|
||||
@@ -450,7 +446,6 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
"content": document.content,
|
||||
"creator": str(document.creator.id),
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"deleted_at": None,
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
@@ -533,7 +528,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"creator": str(document.creator.id),
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
"link_reach": "restricted",
|
||||
@@ -689,7 +683,6 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
@@ -756,7 +749,6 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
@@ -823,7 +815,6 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"deleted_at": None,
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"is_favorite": False,
|
||||
|
||||
234
src/backend/core/tests/documents/test_api_documents_search.py
Normal file
234
src/backend/core/tests/documents/test_api_documents_search.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: list
|
||||
"""
|
||||
|
||||
from json import loads as json_loads
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from faker import Faker
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core.services.search_indexers import get_document_indexer
|
||||
|
||||
fake = Faker()
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
|
||||
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||
@responses.activate
|
||||
def test_api_documents_search_anonymous(reach, role, indexer_settings):
|
||||
"""
|
||||
Anonymous users should not be allowed to search documents whatever the
|
||||
link reach and link role
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
|
||||
|
||||
factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||
|
||||
# Find response
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"http://find/api/v1.0/search",
|
||||
json=[],
|
||||
status=200,
|
||||
)
|
||||
|
||||
response = APIClient().get("/api/v1.0/documents/search/", data={"q": "alpha"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_search_endpoint_is_none(indexer_settings):
|
||||
"""
|
||||
Missing SEARCH_INDEXER_QUERY_URL, so the indexer is not properly configured.
|
||||
Should fallback on title filter
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = None
|
||||
|
||||
assert get_document_indexer() is None
|
||||
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory(title="alpha")
|
||||
access = factories.UserDocumentAccessFactory(document=document, user=user)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/documents/search/", data={"q": "alpha"})
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
results = content.pop("results")
|
||||
assert content == {
|
||||
"count": 1,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
}
|
||||
assert len(results) == 1
|
||||
assert results[0] == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"link_reach": document.link_reach,
|
||||
"link_role": document.link_role,
|
||||
"nb_accesses_ancestors": 1,
|
||||
"nb_accesses_direct": 1,
|
||||
"numchild": 0,
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_search_invalid_params(indexer_settings):
|
||||
"""Validate the format of documents as returned by the search view."""
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/documents/search/")
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"q": ["This field is required."]}
|
||||
|
||||
response = client.get("/api/v1.0/documents/search/", data={"q": " "})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"q": ["This field may not be blank."]}
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/search/", data={"q": "any", "page": "NaN"}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"page": ["A valid integer is required."]}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_search_format(indexer_settings):
|
||||
"""Validate the format of documents as returned by the search view."""
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
|
||||
|
||||
assert get_document_indexer() is not None
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
user_a, user_b, user_c = factories.UserFactory.create_batch(3)
|
||||
document = factories.DocumentFactory(
|
||||
title="alpha",
|
||||
users=(user_a, user_c),
|
||||
link_traces=(user, user_b),
|
||||
)
|
||||
access = factories.UserDocumentAccessFactory(document=document, user=user)
|
||||
|
||||
# Find response
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"http://find/api/v1.0/search",
|
||||
json=[
|
||||
{"_id": str(document.pk)},
|
||||
],
|
||||
status=200,
|
||||
)
|
||||
response = client.get("/api/v1.0/documents/search/", data={"q": "alpha"})
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
results = content.pop("results")
|
||||
assert content == {
|
||||
"count": 1,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
}
|
||||
assert len(results) == 1
|
||||
assert results[0] == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"link_reach": document.link_reach,
|
||||
"link_role": document.link_role,
|
||||
"nb_accesses_ancestors": 3,
|
||||
"nb_accesses_direct": 3,
|
||||
"numchild": 0,
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_search_pagination(indexer_settings):
|
||||
"""Documents should be ordered by descending "updated_at" by default"""
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = "http://find/api/v1.0/search"
|
||||
|
||||
assert get_document_indexer() is not None
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
docs = factories.DocumentFactory.create_batch(10)
|
||||
|
||||
# Find response
|
||||
# pylint: disable-next=assignment-from-none
|
||||
api_search = responses.add(
|
||||
responses.POST,
|
||||
"http://find/api/v1.0/search",
|
||||
json=[{"_id": str(doc.pk)} for doc in docs],
|
||||
status=200,
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/search/", data={"q": "alpha", "page": 2, "page_size": 5}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
results = content.pop("results")
|
||||
assert len(results) == 5
|
||||
|
||||
# Check the query parameters.
|
||||
assert api_search.call_count == 1
|
||||
assert api_search.calls[0].response.status_code == 200
|
||||
assert json_loads(api_search.calls[0].request.body) == {
|
||||
"q": "alpha",
|
||||
"visited": [],
|
||||
"services": ["docs"],
|
||||
"page_number": 2,
|
||||
"page_size": 5,
|
||||
"order_by": "updated_at",
|
||||
"order_direction": "desc",
|
||||
}
|
||||
@@ -48,11 +48,11 @@ def test_api_documents_trashbin_format():
|
||||
|
||||
other_users = factories.UserFactory.create_batch(3)
|
||||
document = factories.DocumentFactory(
|
||||
deleted_at=timezone.now(),
|
||||
users=factories.UserFactory.create_batch(2),
|
||||
favorited_by=[user, *other_users],
|
||||
link_traces=other_users,
|
||||
)
|
||||
document.soft_delete()
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
@@ -70,40 +70,40 @@ def test_api_documents_trashbin_format():
|
||||
assert results[0] == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
"accesses_manage": False,
|
||||
"accesses_view": False,
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": False,
|
||||
"collaboration_auth": False,
|
||||
"descendants": False,
|
||||
"cors_proxy": False,
|
||||
"content": False,
|
||||
"destroy": False,
|
||||
"duplicate": False,
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"accesses_manage": True,
|
||||
"accesses_view": True,
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"attachment_upload": True,
|
||||
"can_edit": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": True,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": True,
|
||||
"link_configuration": True,
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": False,
|
||||
"media_auth": False,
|
||||
"media_check": False,
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False, # Can't move a deleted document
|
||||
"partial_update": False,
|
||||
"partial_update": True,
|
||||
"restore": True,
|
||||
"retrieve": True,
|
||||
"tree": True,
|
||||
"update": False,
|
||||
"versions_destroy": False,
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
"update": True,
|
||||
"versions_destroy": True,
|
||||
"versions_list": True,
|
||||
"versions_retrieve": True,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
@@ -113,7 +113,6 @@ def test_api_documents_trashbin_format():
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
"excerpt": document.excerpt,
|
||||
"deleted_at": document.ancestors_deleted_at.isoformat().replace("+00:00", "Z"),
|
||||
"link_reach": document.link_reach,
|
||||
"link_role": document.link_role,
|
||||
"nb_accesses_ancestors": 0,
|
||||
@@ -166,10 +165,10 @@ def test_api_documents_trashbin_authenticated_direct(django_assert_num_queries):
|
||||
|
||||
expected_ids = {str(document1.id), str(document2.id), str(document3.id)}
|
||||
|
||||
with django_assert_num_queries(11):
|
||||
with django_assert_num_queries(10):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
with django_assert_num_queries(5):
|
||||
with django_assert_num_queries(4):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -208,10 +207,10 @@ def test_api_documents_trashbin_authenticated_via_team(
|
||||
|
||||
expected_ids = {str(deleted_document_team1.id), str(deleted_document_team2.id)}
|
||||
|
||||
with django_assert_num_queries(8):
|
||||
with django_assert_num_queries(7):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(3):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -50,7 +50,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
),
|
||||
"creator": str(child.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": child.excerpt,
|
||||
"id": str(child.id),
|
||||
"is_favorite": False,
|
||||
@@ -74,7 +73,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"id": str(document.id),
|
||||
"is_favorite": False,
|
||||
@@ -98,7 +96,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling1.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": sibling1.excerpt,
|
||||
"id": str(sibling1.id),
|
||||
"is_favorite": False,
|
||||
@@ -122,7 +119,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling2.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": sibling2.excerpt,
|
||||
"id": str(sibling2.id),
|
||||
"is_favorite": False,
|
||||
@@ -142,7 +138,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent.excerpt,
|
||||
"id": str(parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -215,7 +210,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
),
|
||||
"creator": str(child.creator.id),
|
||||
"depth": 5,
|
||||
"deleted_at": None,
|
||||
"excerpt": child.excerpt,
|
||||
"id": str(child.id),
|
||||
"is_favorite": False,
|
||||
@@ -239,7 +233,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 4,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"id": str(document.id),
|
||||
"is_favorite": False,
|
||||
@@ -267,7 +260,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
),
|
||||
"creator": str(document_sibling.creator.id),
|
||||
"depth": 4,
|
||||
"deleted_at": None,
|
||||
"excerpt": document_sibling.excerpt,
|
||||
"id": str(document_sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -289,7 +281,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent.excerpt,
|
||||
"id": str(parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -315,7 +306,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
),
|
||||
"creator": str(parent_sibling.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent_sibling.excerpt,
|
||||
"id": str(parent_sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -337,7 +327,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": grand_parent.excerpt,
|
||||
"id": str(grand_parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -417,7 +406,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
),
|
||||
"creator": str(child.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": child.excerpt,
|
||||
"id": str(child.id),
|
||||
"is_favorite": False,
|
||||
@@ -439,7 +427,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"id": str(document.id),
|
||||
"is_favorite": False,
|
||||
@@ -463,7 +450,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": sibling.excerpt,
|
||||
"id": str(sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -483,7 +469,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent.excerpt,
|
||||
"id": str(parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -561,7 +546,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
),
|
||||
"creator": str(child.creator.id),
|
||||
"depth": 5,
|
||||
"deleted_at": None,
|
||||
"excerpt": child.excerpt,
|
||||
"id": str(child.id),
|
||||
"is_favorite": False,
|
||||
@@ -585,7 +569,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 4,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"id": str(document.id),
|
||||
"is_favorite": False,
|
||||
@@ -613,7 +596,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
),
|
||||
"creator": str(document_sibling.creator.id),
|
||||
"depth": 4,
|
||||
"deleted_at": None,
|
||||
"excerpt": document_sibling.excerpt,
|
||||
"id": str(document_sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -635,7 +617,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent.excerpt,
|
||||
"id": str(parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -661,7 +642,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
),
|
||||
"creator": str(parent_sibling.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent_sibling.excerpt,
|
||||
"id": str(parent_sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -683,7 +663,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": grand_parent.excerpt,
|
||||
"id": str(grand_parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -765,7 +744,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
),
|
||||
"creator": str(child.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": child.excerpt,
|
||||
"id": str(child.id),
|
||||
"is_favorite": False,
|
||||
@@ -787,7 +765,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"id": str(document.id),
|
||||
"is_favorite": False,
|
||||
@@ -811,7 +788,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": sibling.excerpt,
|
||||
"id": str(sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -831,7 +807,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent.excerpt,
|
||||
"id": str(parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -913,7 +888,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
),
|
||||
"creator": str(child.creator.id),
|
||||
"depth": 5,
|
||||
"deleted_at": None,
|
||||
"excerpt": child.excerpt,
|
||||
"id": str(child.id),
|
||||
"is_favorite": False,
|
||||
@@ -937,7 +911,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 4,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"id": str(document.id),
|
||||
"is_favorite": False,
|
||||
@@ -965,7 +938,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
),
|
||||
"creator": str(document_sibling.creator.id),
|
||||
"depth": 4,
|
||||
"deleted_at": None,
|
||||
"excerpt": document_sibling.excerpt,
|
||||
"id": str(document_sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -987,7 +959,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent.excerpt,
|
||||
"id": str(parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -1013,7 +984,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
),
|
||||
"creator": str(parent_sibling.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent_sibling.excerpt,
|
||||
"id": str(parent_sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -1035,7 +1005,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": grand_parent.excerpt,
|
||||
"id": str(grand_parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -1125,7 +1094,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
),
|
||||
"creator": str(child.creator.id),
|
||||
"depth": 3,
|
||||
"deleted_at": None,
|
||||
"excerpt": child.excerpt,
|
||||
"id": str(child.id),
|
||||
"is_favorite": False,
|
||||
@@ -1147,7 +1115,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": document.excerpt,
|
||||
"id": str(document.id),
|
||||
"is_favorite": False,
|
||||
@@ -1171,7 +1138,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
"deleted_at": None,
|
||||
"excerpt": sibling.excerpt,
|
||||
"id": str(sibling.id),
|
||||
"is_favorite": False,
|
||||
@@ -1191,7 +1157,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
"deleted_at": None,
|
||||
"excerpt": parent.excerpt,
|
||||
"id": str(parent.id),
|
||||
"is_favorite": False,
|
||||
@@ -1205,56 +1170,3 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_tree_list_deleted_document():
|
||||
"""
|
||||
Tree of a deleted document should only be accessible to the owner.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(link_reach="public")
|
||||
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
|
||||
factories.DocumentFactory(link_reach="public", parent=document)
|
||||
|
||||
document.soft_delete()
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_api_documents_tree_list_deleted_document_owner(django_assert_num_queries):
|
||||
"""
|
||||
Tree of a deleted document should only be accessible to the owner.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(link_reach="public", users=[(user, "owner")])
|
||||
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
|
||||
document.soft_delete()
|
||||
document.refresh_from_db()
|
||||
child.refresh_from_db()
|
||||
|
||||
with django_assert_num_queries(9):
|
||||
client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||
|
||||
with django_assert_num_queries(5):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert content["id"] == str(document.id)
|
||||
assert content["deleted_at"] == document.deleted_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
)
|
||||
assert len(content["children"]) == 1
|
||||
assert content["children"][0]["id"] == str(child.id)
|
||||
assert content["children"][0][
|
||||
"deleted_at"
|
||||
] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z")
|
||||
|
||||
@@ -42,7 +42,6 @@ def test_api_config(is_authenticated):
|
||||
response = client.get("/api/v1.0/config/")
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"AI_FEATURE_ENABLED": False,
|
||||
"COLLABORATION_WS_URL": "http://testcollab/",
|
||||
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
|
||||
"CRISP_WEBSITE_ID": "123",
|
||||
@@ -61,7 +60,7 @@ def test_api_config(is_authenticated):
|
||||
"MEDIA_BASE_URL": "http://testserver/",
|
||||
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
|
||||
"SENTRY_DSN": "https://sentry.test/123",
|
||||
"TRASHBIN_CUTOFF_DAYS": 30,
|
||||
"AI_FEATURE_ENABLED": False,
|
||||
"theme_customization": {},
|
||||
}
|
||||
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
|
||||
|
||||
@@ -6,6 +6,7 @@ Unit tests for the Document model
|
||||
import random
|
||||
import smtplib
|
||||
from logging import Logger
|
||||
from operator import itemgetter
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
@@ -13,12 +14,14 @@ from django.core import mail
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import transaction
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories, models
|
||||
from core.services.search_indexers import FindDocumentIndexer
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -375,42 +378,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
|
||||
document.soft_delete()
|
||||
document.refresh_from_db()
|
||||
assert document.get_abilities(user) == {
|
||||
"accesses_manage": False,
|
||||
"accesses_view": False,
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": False,
|
||||
"collaboration_auth": False,
|
||||
"descendants": False,
|
||||
"cors_proxy": False,
|
||||
"content": False,
|
||||
"destroy": False,
|
||||
"duplicate": False,
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": False,
|
||||
"media_auth": False,
|
||||
"media_check": False,
|
||||
"move": False,
|
||||
"partial_update": False,
|
||||
"restore": True,
|
||||
"retrieve": True,
|
||||
"tree": True,
|
||||
"update": False,
|
||||
"versions_destroy": False,
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
}
|
||||
expected_abilities["move"] = False
|
||||
assert document.get_abilities(user) == expected_abilities
|
||||
|
||||
|
||||
@override_settings(
|
||||
@@ -1445,3 +1414,285 @@ def test_models_documents_compute_ancestors_links_paths_mapping_structure(
|
||||
{"link_reach": sibling.link_reach, "link_role": sibling.link_role},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@mock.patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_models_documents_post_save_indexer(mock_push, indexer_settings):
|
||||
"""Test indexation task on document creation"""
|
||||
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
|
||||
|
||||
with transaction.atomic():
|
||||
doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3)
|
||||
|
||||
accesses = {}
|
||||
data = [call.args[0] for call in mock_push.call_args_list]
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
|
||||
assert sorted(data, key=itemgetter("id")) == sorted(
|
||||
[
|
||||
indexer.serialize_document(doc1, accesses),
|
||||
indexer.serialize_document(doc2, accesses),
|
||||
indexer.serialize_document(doc3, accesses),
|
||||
],
|
||||
key=itemgetter("id"),
|
||||
)
|
||||
|
||||
# The debounce counters should be reset
|
||||
assert cache.get(f"doc-indexer-debounce-{doc1.pk}") == 0
|
||||
assert cache.get(f"doc-indexer-debounce-{doc2.pk}") == 0
|
||||
assert cache.get(f"doc-indexer-debounce-{doc3.pk}") == 0
|
||||
|
||||
|
||||
@mock.patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_models_documents_post_save_indexer_not_configured(mock_push, indexer_settings):
|
||||
"""Task should not start an indexation when disabled"""
|
||||
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
|
||||
indexer_settings.SEARCH_INDEXER_CLASS = None
|
||||
|
||||
with transaction.atomic():
|
||||
factories.DocumentFactory()
|
||||
|
||||
assert mock_push.call_args_list == []
|
||||
|
||||
|
||||
@mock.patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_models_documents_post_save_indexer_with_accesses(mock_push, indexer_settings):
|
||||
"""Test indexation task on document creation"""
|
||||
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
with transaction.atomic():
|
||||
doc1, doc2, doc3 = factories.DocumentFactory.create_batch(3)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=doc1, user=user)
|
||||
factories.UserDocumentAccessFactory(document=doc2, user=user)
|
||||
factories.UserDocumentAccessFactory(document=doc3, user=user)
|
||||
|
||||
accesses = {
|
||||
str(doc1.path): {"users": [user.sub]},
|
||||
str(doc2.path): {"users": [user.sub]},
|
||||
str(doc3.path): {"users": [user.sub]},
|
||||
}
|
||||
|
||||
data = [call.args[0] for call in mock_push.call_args_list]
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
|
||||
assert sorted(data, key=itemgetter("id")) == sorted(
|
||||
[
|
||||
indexer.serialize_document(doc1, accesses),
|
||||
indexer.serialize_document(doc2, accesses),
|
||||
indexer.serialize_document(doc3, accesses),
|
||||
],
|
||||
key=itemgetter("id"),
|
||||
)
|
||||
|
||||
# The debounce counters should be reset
|
||||
assert cache.get(f"doc-indexer-debounce-{doc1.pk}") == 0
|
||||
assert cache.get(f"doc-indexer-debounce-{doc2.pk}") == 0
|
||||
assert cache.get(f"doc-indexer-debounce-{doc3.pk}") == 0
|
||||
|
||||
|
||||
@mock.patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_models_documents_post_save_indexer_deleted(mock_push, indexer_settings):
|
||||
"""Indexation task on deleted or ancestor_deleted documents"""
|
||||
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
with transaction.atomic():
|
||||
doc = factories.DocumentFactory(
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED
|
||||
)
|
||||
doc_deleted = factories.DocumentFactory(
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED
|
||||
)
|
||||
doc_ancestor_deleted = factories.DocumentFactory(
|
||||
parent=doc_deleted,
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED,
|
||||
)
|
||||
doc_deleted.soft_delete()
|
||||
doc_ancestor_deleted.ancestors_deleted_at = doc_deleted.deleted_at
|
||||
|
||||
factories.UserDocumentAccessFactory(document=doc, user=user)
|
||||
factories.UserDocumentAccessFactory(document=doc_deleted, user=user)
|
||||
factories.UserDocumentAccessFactory(document=doc_ancestor_deleted, user=user)
|
||||
|
||||
doc_deleted.refresh_from_db()
|
||||
doc_ancestor_deleted.refresh_from_db()
|
||||
|
||||
assert doc_deleted.deleted_at is not None
|
||||
assert doc_deleted.ancestors_deleted_at is not None
|
||||
|
||||
assert doc_ancestor_deleted.deleted_at is None
|
||||
assert doc_ancestor_deleted.ancestors_deleted_at is not None
|
||||
|
||||
accesses = {
|
||||
str(doc.path): {"users": [user.sub]},
|
||||
str(doc_deleted.path): {"users": [user.sub]},
|
||||
str(doc_ancestor_deleted.path): {"users": [user.sub]},
|
||||
}
|
||||
|
||||
data = [call.args[0] for call in mock_push.call_args_list]
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
|
||||
# Even deleted document are re-indexed : only update their status in the future ?
|
||||
assert sorted(data, key=itemgetter("id")) == sorted(
|
||||
[
|
||||
indexer.serialize_document(doc, accesses),
|
||||
indexer.serialize_document(doc_deleted, accesses),
|
||||
indexer.serialize_document(doc_ancestor_deleted, accesses),
|
||||
indexer.serialize_document(doc_deleted, accesses), # soft_delete()
|
||||
],
|
||||
key=itemgetter("id"),
|
||||
)
|
||||
|
||||
# The debounce counters should be reset
|
||||
assert cache.get(f"doc-indexer-debounce-{doc.pk}") == 0
|
||||
assert cache.get(f"doc-indexer-debounce-{doc_deleted.pk}") == 0
|
||||
assert cache.get(f"doc-indexer-debounce-{doc_ancestor_deleted.pk}") == 0
|
||||
|
||||
|
||||
@mock.patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_models_documents_post_save_indexer_restored(mock_push, indexer_settings):
|
||||
"""Restart indexation task on restored documents"""
|
||||
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
with transaction.atomic():
|
||||
doc = factories.DocumentFactory(
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED
|
||||
)
|
||||
doc_deleted = factories.DocumentFactory(
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED
|
||||
)
|
||||
doc_ancestor_deleted = factories.DocumentFactory(
|
||||
parent=doc_deleted,
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED,
|
||||
)
|
||||
doc_deleted.soft_delete()
|
||||
doc_ancestor_deleted.ancestors_deleted_at = doc_deleted.deleted_at
|
||||
|
||||
factories.UserDocumentAccessFactory(document=doc, user=user)
|
||||
factories.UserDocumentAccessFactory(document=doc_deleted, user=user)
|
||||
factories.UserDocumentAccessFactory(document=doc_ancestor_deleted, user=user)
|
||||
|
||||
doc_deleted.refresh_from_db()
|
||||
doc_ancestor_deleted.refresh_from_db()
|
||||
|
||||
assert doc_deleted.deleted_at is not None
|
||||
assert doc_deleted.ancestors_deleted_at is not None
|
||||
|
||||
assert doc_ancestor_deleted.deleted_at is None
|
||||
assert doc_ancestor_deleted.ancestors_deleted_at is not None
|
||||
|
||||
doc_restored = models.Document.objects.get(pk=doc_deleted.pk)
|
||||
doc_restored.restore()
|
||||
|
||||
doc_ancestor_restored = models.Document.objects.get(pk=doc_ancestor_deleted.pk)
|
||||
|
||||
assert doc_restored.deleted_at is None
|
||||
assert doc_restored.ancestors_deleted_at is None
|
||||
|
||||
assert doc_ancestor_restored.deleted_at is None
|
||||
assert doc_ancestor_restored.ancestors_deleted_at is None
|
||||
|
||||
accesses = {
|
||||
str(doc.path): {"users": [user.sub]},
|
||||
str(doc_deleted.path): {"users": [user.sub]},
|
||||
str(doc_ancestor_deleted.path): {"users": [user.sub]},
|
||||
}
|
||||
|
||||
data = [call.args[0] for call in mock_push.call_args_list]
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
|
||||
# All docs are re-indexed
|
||||
assert sorted(data, key=itemgetter("id")) == sorted(
|
||||
[
|
||||
indexer.serialize_document(doc, accesses),
|
||||
indexer.serialize_document(doc_deleted, accesses),
|
||||
indexer.serialize_document(doc_deleted, accesses), # soft_delete()
|
||||
indexer.serialize_document(doc_restored, accesses), # restore()
|
||||
indexer.serialize_document(doc_ancestor_deleted, accesses),
|
||||
],
|
||||
key=itemgetter("id"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_models_documents_post_save_indexer_debounce(indexer_settings):
|
||||
"""Test indexation task skipping on document update"""
|
||||
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
user = factories.UserFactory()
|
||||
|
||||
with mock.patch.object(FindDocumentIndexer, "push"):
|
||||
with transaction.atomic():
|
||||
doc = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=doc, user=user)
|
||||
|
||||
accesses = {
|
||||
str(doc.path): {"users": [user.sub]},
|
||||
}
|
||||
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
# Simulate 1 waiting task
|
||||
cache.set(f"doc-indexer-debounce-{doc.pk}", 1)
|
||||
|
||||
# save doc to trigger the indexer, but nothing should be done since
|
||||
# the counter is over 0
|
||||
with transaction.atomic():
|
||||
doc.save()
|
||||
|
||||
assert [call.args[0] for call in mock_push.call_args_list] == []
|
||||
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
# No waiting task
|
||||
cache.set(f"doc-indexer-debounce-{doc.pk}", 0)
|
||||
|
||||
with transaction.atomic():
|
||||
doc = models.Document.objects.get(pk=doc.pk)
|
||||
doc.save()
|
||||
|
||||
assert [call.args[0] for call in mock_push.call_args_list] == [
|
||||
indexer.serialize_document(doc, accesses),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_models_documents_access_post_save_indexer(indexer_settings):
|
||||
"""Test indexation task on DocumentAccess update"""
|
||||
indexer_settings.SEARCH_INDEXER_COUNTDOWN = 0
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
user = factories.UserFactory()
|
||||
|
||||
with mock.patch.object(FindDocumentIndexer, "push"):
|
||||
with transaction.atomic():
|
||||
doc = factories.DocumentFactory()
|
||||
doc_access = factories.UserDocumentAccessFactory(document=doc, user=user)
|
||||
|
||||
accesses = {
|
||||
str(doc.path): {"users": [user.sub]},
|
||||
}
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
|
||||
with mock.patch.object(FindDocumentIndexer, "push") as mock_push:
|
||||
with transaction.atomic():
|
||||
doc_access.save()
|
||||
|
||||
assert [call.args[0] for call in mock_push.call_args_list] == [
|
||||
indexer.serialize_document(doc, accesses),
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories, models
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -66,33 +66,3 @@ def test_models_users_sub_validator(sub, is_valid):
|
||||
match=("Enter a valid sub. This value should be ASCII only."),
|
||||
):
|
||||
user.full_clean()
|
||||
|
||||
|
||||
def test_modes_users_convert_valid_invitations():
|
||||
"""
|
||||
The "convert_valid_invitations" method should convert valid invitations to document accesses.
|
||||
"""
|
||||
email = "test@example.com"
|
||||
document = factories.DocumentFactory()
|
||||
other_document = factories.DocumentFactory()
|
||||
invitation_document = factories.InvitationFactory(email=email, document=document)
|
||||
invitation_other_document = factories.InvitationFactory(
|
||||
email="Test@example.coM", document=other_document
|
||||
)
|
||||
other_email_invitation = factories.InvitationFactory(
|
||||
email="pre_test@example.com", document=document
|
||||
)
|
||||
|
||||
assert document.accesses.count() == 0
|
||||
assert other_document.accesses.count() == 0
|
||||
|
||||
user = factories.UserFactory(email=email)
|
||||
|
||||
assert document.accesses.filter(user=user).count() == 1
|
||||
assert other_document.accesses.filter(user=user).count() == 1
|
||||
|
||||
assert not models.Invitation.objects.filter(id=invitation_document.id).exists()
|
||||
assert not models.Invitation.objects.filter(
|
||||
id=invitation_other_document.id
|
||||
).exists()
|
||||
assert models.Invitation.objects.filter(id=other_email_invitation.id).exists()
|
||||
|
||||
540
src/backend/core/tests/test_services_search_indexers.py
Normal file
540
src/backend/core/tests/test_services_search_indexers.py
Normal file
@@ -0,0 +1,540 @@
|
||||
"""Tests for Documents search indexers"""
|
||||
|
||||
from functools import partial
|
||||
from json import dumps as json_dumps
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from requests import HTTPError
|
||||
|
||||
from core import factories, models, utils
|
||||
from core.services.search_indexers import (
|
||||
BaseDocumentIndexer,
|
||||
FindDocumentIndexer,
|
||||
get_document_indexer,
|
||||
get_visited_document_ids_of,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
class FakeDocumentIndexer(BaseDocumentIndexer):
|
||||
"""Fake indexer for test purpose"""
|
||||
|
||||
def serialize_document(self, document, accesses):
|
||||
return {}
|
||||
|
||||
def push(self, data):
|
||||
pass
|
||||
|
||||
def search_query(self, data, token):
|
||||
return {}
|
||||
|
||||
|
||||
def test_services_search_indexer_class_invalid(indexer_settings):
|
||||
"""
|
||||
Should raise RuntimeError if SEARCH_INDEXER_CLASS cannot be imported.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_CLASS = "unknown.Unknown"
|
||||
|
||||
assert get_document_indexer() is None
|
||||
|
||||
|
||||
def test_services_search_indexer_class(indexer_settings):
|
||||
"""
|
||||
Import indexer class defined in setting SEARCH_INDEXER_CLASS.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_CLASS = (
|
||||
"core.tests.test_services_search_indexers.FakeDocumentIndexer"
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
get_document_indexer(),
|
||||
import_string("core.tests.test_services_search_indexers.FakeDocumentIndexer"),
|
||||
)
|
||||
|
||||
|
||||
def test_services_search_indexer_is_configured(indexer_settings):
|
||||
"""
|
||||
Should return true only when the indexer class and other configuration settings
|
||||
are valid.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_CLASS = None
|
||||
|
||||
# None
|
||||
get_document_indexer.cache_clear()
|
||||
assert not get_document_indexer()
|
||||
|
||||
# Empty
|
||||
indexer_settings.SEARCH_INDEXER_CLASS = ""
|
||||
|
||||
get_document_indexer.cache_clear()
|
||||
assert not get_document_indexer()
|
||||
|
||||
# Valid class
|
||||
indexer_settings.SEARCH_INDEXER_CLASS = (
|
||||
"core.services.search_indexers.FindDocumentIndexer"
|
||||
)
|
||||
|
||||
get_document_indexer.cache_clear()
|
||||
assert get_document_indexer() is not None
|
||||
|
||||
indexer_settings.SEARCH_INDEXER_URL = ""
|
||||
|
||||
# Invalid url
|
||||
get_document_indexer.cache_clear()
|
||||
assert not get_document_indexer()
|
||||
|
||||
|
||||
def test_services_search_indexer_url_is_none(indexer_settings):
|
||||
"""
|
||||
Indexer should raise RuntimeError if SEARCH_INDEXER_URL is None or empty.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_URL = None
|
||||
|
||||
with pytest.raises(ImproperlyConfigured) as exc_info:
|
||||
FindDocumentIndexer()
|
||||
|
||||
assert "SEARCH_INDEXER_URL must be set in Django settings." in str(exc_info.value)
|
||||
|
||||
|
||||
def test_services_search_indexer_url_is_empty(indexer_settings):
|
||||
"""
|
||||
Indexer should raise RuntimeError if SEARCH_INDEXER_URL is empty string.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_URL = ""
|
||||
|
||||
with pytest.raises(ImproperlyConfigured) as exc_info:
|
||||
FindDocumentIndexer()
|
||||
|
||||
assert "SEARCH_INDEXER_URL must be set in Django settings." in str(exc_info.value)
|
||||
|
||||
|
||||
def test_services_search_indexer_secret_is_none(indexer_settings):
|
||||
"""
|
||||
Indexer should raise RuntimeError if SEARCH_INDEXER_SECRET is None.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_SECRET = None
|
||||
|
||||
with pytest.raises(ImproperlyConfigured) as exc_info:
|
||||
FindDocumentIndexer()
|
||||
|
||||
assert "SEARCH_INDEXER_SECRET must be set in Django settings." in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
|
||||
def test_services_search_indexer_secret_is_empty(indexer_settings):
|
||||
"""
|
||||
Indexer should raise RuntimeError if SEARCH_INDEXER_SECRET is empty string.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_SECRET = ""
|
||||
|
||||
with pytest.raises(ImproperlyConfigured) as exc_info:
|
||||
FindDocumentIndexer()
|
||||
|
||||
assert "SEARCH_INDEXER_SECRET must be set in Django settings." in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
|
||||
def test_services_search_endpoint_is_none(indexer_settings):
|
||||
"""
|
||||
Indexer should raise RuntimeError if SEARCH_INDEXER_QUERY_URL is None.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = None
|
||||
|
||||
with pytest.raises(ImproperlyConfigured) as exc_info:
|
||||
FindDocumentIndexer()
|
||||
|
||||
assert "SEARCH_INDEXER_QUERY_URL must be set in Django settings." in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
|
||||
def test_services_search_endpoint_is_empty(indexer_settings):
|
||||
"""
|
||||
Indexer should raise RuntimeError if SEARCH_INDEXER_QUERY_URL is empty.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = ""
|
||||
|
||||
with pytest.raises(ImproperlyConfigured) as exc_info:
|
||||
FindDocumentIndexer()
|
||||
|
||||
assert "SEARCH_INDEXER_QUERY_URL must be set in Django settings." in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_services_search_indexers_serialize_document_returns_expected_json():
|
||||
"""
|
||||
It should serialize documents with correct metadata and access control.
|
||||
"""
|
||||
user_a, user_b = factories.UserFactory.create_batch(2)
|
||||
document = factories.DocumentFactory()
|
||||
factories.DocumentFactory(parent=document)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=document, user=user_a)
|
||||
factories.UserDocumentAccessFactory(document=document, user=user_b)
|
||||
factories.TeamDocumentAccessFactory(document=document, team="team1")
|
||||
factories.TeamDocumentAccessFactory(document=document, team="team2")
|
||||
|
||||
accesses = {
|
||||
document.path: {
|
||||
"users": {str(user_a.sub), str(user_b.sub)},
|
||||
"teams": {"team1", "team2"},
|
||||
}
|
||||
}
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
result = indexer.serialize_document(document, accesses)
|
||||
|
||||
assert set(result.pop("users")) == {str(user_a.sub), str(user_b.sub)}
|
||||
assert set(result.pop("groups")) == {"team1", "team2"}
|
||||
assert result == {
|
||||
"id": str(document.id),
|
||||
"title": document.title,
|
||||
"depth": 1,
|
||||
"path": document.path,
|
||||
"numchild": 1,
|
||||
"content": utils.base64_yjs_to_text(document.content),
|
||||
"created_at": document.created_at.isoformat(),
|
||||
"updated_at": document.updated_at.isoformat(),
|
||||
"reach": document.link_reach,
|
||||
"size": 13,
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_services_search_indexers_serialize_document_deleted():
|
||||
"""Deleted documents are marked as just in the serialized json."""
|
||||
parent = factories.DocumentFactory()
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
|
||||
parent.soft_delete()
|
||||
document.refresh_from_db()
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
result = indexer.serialize_document(document, {})
|
||||
|
||||
assert result["is_active"] is False
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_services_search_indexers_serialize_document_empty():
|
||||
"""Empty documents returns empty content in the serialized json."""
|
||||
document = factories.DocumentFactory(content="", title=None)
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
result = indexer.serialize_document(document, {})
|
||||
|
||||
assert result["content"] == ""
|
||||
assert result["title"] == ""
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_services_search_indexers_index_errors(indexer_settings):
|
||||
"""
|
||||
Documents indexing response handling on Find API HTTP errors.
|
||||
"""
|
||||
factories.DocumentFactory()
|
||||
|
||||
indexer_settings.SEARCH_INDEXER_URL = "http://app-find/api/v1.0/documents/index/"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"http://app-find/api/v1.0/documents/index/",
|
||||
status=401,
|
||||
body=json_dumps({"message": "Authentication failed."}),
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPError):
|
||||
FindDocumentIndexer().index()
|
||||
|
||||
|
||||
@patch.object(FindDocumentIndexer, "push")
|
||||
def test_services_search_indexers_batches_pass_only_batch_accesses(
|
||||
mock_push, indexer_settings
|
||||
):
|
||||
"""
|
||||
Documents indexing should be processed in batches,
|
||||
and only the access data relevant to each batch should be used.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_BATCH_SIZE = 2
|
||||
documents = factories.DocumentFactory.create_batch(5)
|
||||
|
||||
# Attach a single user access to each document
|
||||
expected_user_subs = {}
|
||||
for document in documents:
|
||||
access = factories.UserDocumentAccessFactory(document=document)
|
||||
expected_user_subs[str(document.id)] = str(access.user.sub)
|
||||
|
||||
assert FindDocumentIndexer().index() == 5
|
||||
|
||||
# Should be 3 batches: 2 + 2 + 1
|
||||
assert mock_push.call_count == 3
|
||||
|
||||
seen_doc_ids = set()
|
||||
|
||||
for call in mock_push.call_args_list:
|
||||
batch = call.args[0]
|
||||
assert isinstance(batch, list)
|
||||
|
||||
for doc_json in batch:
|
||||
doc_id = doc_json["id"]
|
||||
seen_doc_ids.add(doc_id)
|
||||
|
||||
# Only one user expected per document
|
||||
assert doc_json["users"] == [expected_user_subs[doc_id]]
|
||||
assert doc_json["groups"] == []
|
||||
|
||||
# Make sure all 5 documents were indexed
|
||||
assert seen_doc_ids == {str(d.id) for d in documents}
|
||||
|
||||
|
||||
@patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_services_search_indexers_ignore_empty_documents(mock_push):
|
||||
"""
|
||||
Documents indexing should be processed in batches,
|
||||
and only the access data relevant to each batch should be used.
|
||||
"""
|
||||
document = factories.DocumentFactory()
|
||||
factories.DocumentFactory(content="", title="")
|
||||
empty_title = factories.DocumentFactory(title="")
|
||||
empty_content = factories.DocumentFactory(content="")
|
||||
|
||||
assert FindDocumentIndexer().index() == 3
|
||||
|
||||
assert mock_push.call_count == 1
|
||||
|
||||
# Make sure only not eempty documents are indexed
|
||||
results = {doc["id"] for doc in mock_push.call_args[0][0]}
|
||||
assert results == {
|
||||
str(d.id)
|
||||
for d in (
|
||||
document,
|
||||
empty_content,
|
||||
empty_title,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_services_search_indexers_ancestors_link_reach(mock_push):
|
||||
"""Document accesses and reach should take into account ancestors link reaches."""
|
||||
great_grand_parent = factories.DocumentFactory(link_reach="restricted")
|
||||
grand_parent = factories.DocumentFactory(
|
||||
parent=great_grand_parent, link_reach="authenticated"
|
||||
)
|
||||
parent = factories.DocumentFactory(parent=grand_parent, link_reach="public")
|
||||
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
|
||||
|
||||
assert FindDocumentIndexer().index() == 4
|
||||
|
||||
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
|
||||
assert len(results) == 4
|
||||
assert results[str(great_grand_parent.id)]["reach"] == "restricted"
|
||||
assert results[str(grand_parent.id)]["reach"] == "authenticated"
|
||||
assert results[str(parent.id)]["reach"] == "public"
|
||||
assert results[str(document.id)]["reach"] == "public"
|
||||
|
||||
|
||||
@patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_services_search_indexers_ancestors_users(mock_push):
|
||||
"""Document accesses and reach should include users from ancestors."""
|
||||
user_gp, user_p, user_d = factories.UserFactory.create_batch(3)
|
||||
|
||||
grand_parent = factories.DocumentFactory(users=[user_gp])
|
||||
parent = factories.DocumentFactory(parent=grand_parent, users=[user_p])
|
||||
document = factories.DocumentFactory(parent=parent, users=[user_d])
|
||||
|
||||
assert FindDocumentIndexer().index() == 3
|
||||
|
||||
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
|
||||
assert len(results) == 3
|
||||
assert results[str(grand_parent.id)]["users"] == [str(user_gp.sub)]
|
||||
assert set(results[str(parent.id)]["users"]) == {str(user_gp.sub), str(user_p.sub)}
|
||||
assert set(results[str(document.id)]["users"]) == {
|
||||
str(user_gp.sub),
|
||||
str(user_p.sub),
|
||||
str(user_d.sub),
|
||||
}
|
||||
|
||||
|
||||
@patch.object(FindDocumentIndexer, "push")
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_services_search_indexers_ancestors_teams(mock_push):
|
||||
"""Document accesses and reach should include teams from ancestors."""
|
||||
grand_parent = factories.DocumentFactory(teams=["team_gp"])
|
||||
parent = factories.DocumentFactory(parent=grand_parent, teams=["team_p"])
|
||||
document = factories.DocumentFactory(parent=parent, teams=["team_d"])
|
||||
|
||||
assert FindDocumentIndexer().index() == 3
|
||||
|
||||
results = {doc["id"]: doc for doc in mock_push.call_args[0][0]}
|
||||
assert len(results) == 3
|
||||
assert results[str(grand_parent.id)]["groups"] == ["team_gp"]
|
||||
assert set(results[str(parent.id)]["groups"]) == {"team_gp", "team_p"}
|
||||
assert set(results[str(document.id)]["groups"]) == {"team_gp", "team_p", "team_d"}
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_push_uses_correct_url_and_data(mock_post, indexer_settings):
|
||||
"""
|
||||
push() should call requests.post with the correct URL from settings
|
||||
the timeout set to 10 seconds and the data as JSON.
|
||||
"""
|
||||
indexer_settings.SEARCH_INDEXER_URL = "http://example.com/index"
|
||||
|
||||
indexer = FindDocumentIndexer()
|
||||
sample_data = [{"id": "123", "title": "Test"}]
|
||||
|
||||
mock_response = mock_post.return_value
|
||||
mock_response.raise_for_status.return_value = None # No error
|
||||
|
||||
indexer.push(sample_data)
|
||||
|
||||
mock_post.assert_called_once()
|
||||
args, kwargs = mock_post.call_args
|
||||
|
||||
assert args[0] == indexer_settings.SEARCH_INDEXER_URL
|
||||
assert kwargs.get("json") == sample_data
|
||||
assert kwargs.get("timeout") == 10
|
||||
|
||||
|
||||
def test_get_visited_document_ids_of():
|
||||
"""
|
||||
get_visited_document_ids_of() returns the ids of the documents viewed
|
||||
by the user BUT without specific access configuration (like public ones)
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
other = factories.UserFactory()
|
||||
anonymous = AnonymousUser()
|
||||
queryset = models.Document.objects.all()
|
||||
|
||||
assert not get_visited_document_ids_of(queryset, anonymous)
|
||||
assert not get_visited_document_ids_of(queryset, user)
|
||||
|
||||
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
|
||||
|
||||
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
|
||||
|
||||
create_link(document=doc1)
|
||||
create_link(document=doc2)
|
||||
|
||||
# The third document is not visited
|
||||
assert sorted(get_visited_document_ids_of(queryset, user)) == sorted(
|
||||
[str(doc1.pk), str(doc2.pk)]
|
||||
)
|
||||
|
||||
factories.UserDocumentAccessFactory(user=other, document=doc1)
|
||||
factories.UserDocumentAccessFactory(user=user, document=doc2)
|
||||
|
||||
# The second document have an access for the user
|
||||
assert get_visited_document_ids_of(queryset, user) == [str(doc1.pk)]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
def test_get_visited_document_ids_of_deleted():
|
||||
"""
|
||||
get_visited_document_ids_of() returns the ids of the documents viewed
|
||||
by the user if they are not deleted.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
anonymous = AnonymousUser()
|
||||
queryset = models.Document.objects.all()
|
||||
|
||||
assert not get_visited_document_ids_of(queryset, anonymous)
|
||||
assert not get_visited_document_ids_of(queryset, user)
|
||||
|
||||
doc = factories.DocumentFactory()
|
||||
doc_deleted = factories.DocumentFactory()
|
||||
doc_ancestor_deleted = factories.DocumentFactory(parent=doc_deleted)
|
||||
|
||||
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
|
||||
|
||||
create_link(document=doc)
|
||||
create_link(document=doc_deleted)
|
||||
create_link(document=doc_ancestor_deleted)
|
||||
|
||||
# The all documents are visited
|
||||
assert sorted(get_visited_document_ids_of(queryset, user)) == sorted(
|
||||
[str(doc.pk), str(doc_deleted.pk), str(doc_ancestor_deleted.pk)]
|
||||
)
|
||||
|
||||
doc_deleted.soft_delete()
|
||||
|
||||
# Only the first document is not deleted
|
||||
assert get_visited_document_ids_of(queryset, user) == [str(doc.pk)]
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_services_search_indexers_search_errors(indexer_settings):
|
||||
"""
|
||||
Documents indexing response handling on Find API HTTP errors.
|
||||
"""
|
||||
factories.DocumentFactory()
|
||||
|
||||
indexer_settings.SEARCH_INDEXER_QUERY_URL = (
|
||||
"http://app-find/api/v1.0/documents/search/"
|
||||
)
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"http://app-find/api/v1.0/documents/search/",
|
||||
status=401,
|
||||
body=json_dumps({"message": "Authentication failed."}),
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPError):
|
||||
FindDocumentIndexer().search("alpha", token="mytoken")
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_services_search_indexers_search(mock_post, indexer_settings):
|
||||
"""
|
||||
search() should call requests.post to SEARCH_INDEXER_QUERY_URL with the
|
||||
document ids from linktraces.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
indexer = FindDocumentIndexer()
|
||||
|
||||
mock_response = mock_post.return_value
|
||||
mock_response.raise_for_status.return_value = None # No error
|
||||
|
||||
doc1, doc2, _ = factories.DocumentFactory.create_batch(3)
|
||||
|
||||
create_link = partial(models.LinkTrace.objects.create, user=user, is_masked=False)
|
||||
|
||||
create_link(document=doc1)
|
||||
create_link(document=doc2)
|
||||
|
||||
visited = get_visited_document_ids_of(models.Document.objects.all(), user)
|
||||
|
||||
indexer.search("alpha", visited=visited, token="mytoken")
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
|
||||
assert args[0] == indexer_settings.SEARCH_INDEXER_QUERY_URL
|
||||
|
||||
query_data = kwargs.get("json")
|
||||
assert query_data["q"] == "alpha"
|
||||
assert sorted(query_data["visited"]) == sorted([str(doc1.pk), str(doc2.pk)])
|
||||
assert query_data["services"] == ["docs"]
|
||||
assert query_data["page_number"] == 1
|
||||
assert query_data["page_size"] == 50
|
||||
assert query_data["order_by"] == "updated_at"
|
||||
assert query_data["order_direction"] == "desc"
|
||||
|
||||
assert kwargs.get("headers") == {"Authorization": "Bearer mytoken"}
|
||||
assert kwargs.get("timeout") == 10
|
||||
@@ -75,3 +75,28 @@ def test_utils_extract_attachments():
|
||||
base64_string = base64.b64encode(update).decode("utf-8")
|
||||
# image_key2 is missing the "/media/" part and shouldn't get extracted
|
||||
assert utils.extract_attachments(base64_string) == [image_key1, image_key3]
|
||||
|
||||
|
||||
def test_utils_get_ancestor_to_descendants_map_single_path():
|
||||
"""Test ancestor mapping of a single path."""
|
||||
paths = ["000100020005"]
|
||||
result = utils.get_ancestor_to_descendants_map(paths, steplen=4)
|
||||
|
||||
assert result == {
|
||||
"0001": {"000100020005"},
|
||||
"00010002": {"000100020005"},
|
||||
"000100020005": {"000100020005"},
|
||||
}
|
||||
|
||||
|
||||
def test_utils_get_ancestor_to_descendants_map_multiple_paths():
|
||||
"""Test ancestor mapping of multiple paths with shared prefixes."""
|
||||
paths = ["000100020005", "00010003"]
|
||||
result = utils.get_ancestor_to_descendants_map(paths, steplen=4)
|
||||
|
||||
assert result == {
|
||||
"0001": {"000100020005", "00010003"},
|
||||
"00010002": {"000100020005"},
|
||||
"000100020005": {"000100020005"},
|
||||
"00010003": {"00010003"},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import base64
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
import pycrdt
|
||||
from bs4 import BeautifulSoup
|
||||
@@ -9,6 +10,27 @@ from bs4 import BeautifulSoup
|
||||
from core import enums
|
||||
|
||||
|
||||
def get_ancestor_to_descendants_map(paths, steplen):
|
||||
"""
|
||||
Given a list of document paths, return a mapping of ancestor_path -> set of descendant_paths.
|
||||
|
||||
Each path is assumed to use materialized path format with fixed-length segments.
|
||||
|
||||
Args:
|
||||
paths (list of str): List of full document paths.
|
||||
steplen (int): Length of each path segment.
|
||||
|
||||
Returns:
|
||||
dict[str, set[str]]: Mapping from ancestor path to its descendant paths (including itself).
|
||||
"""
|
||||
ancestor_map = defaultdict(set)
|
||||
for path in paths:
|
||||
for i in range(steplen, len(path) + 1, steplen):
|
||||
ancestor = path[:i]
|
||||
ancestor_map[ancestor].add(path)
|
||||
return ancestor_map
|
||||
|
||||
|
||||
def filter_descendants(paths, root_paths, skip_sorting=False):
|
||||
"""
|
||||
Filters paths to keep only those that are descendants of any path in root_paths.
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
# ruff: noqa: S311, S106
|
||||
"""create_demo management command"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from uuid import uuid4
|
||||
|
||||
from django import db
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
import pycrdt
|
||||
from faker import Faker
|
||||
|
||||
from core import models
|
||||
@@ -27,6 +30,16 @@ def random_true_with_probability(probability):
|
||||
return random.random() < probability
|
||||
|
||||
|
||||
def get_ydoc_for_text(text):
|
||||
"""Return a ydoc from plain text for demo purposes."""
|
||||
ydoc = pycrdt.Doc()
|
||||
paragraph = pycrdt.XmlElement("p", {}, [pycrdt.XmlText(text)])
|
||||
fragment = pycrdt.XmlFragment([paragraph])
|
||||
ydoc["document-store"] = fragment
|
||||
update = ydoc.get_update()
|
||||
return base64.b64encode(update).decode("utf-8")
|
||||
|
||||
|
||||
class BulkQueue:
|
||||
"""A utility class to create Django model instances in bulk by just pushing to a queue."""
|
||||
|
||||
@@ -48,7 +61,7 @@ class BulkQueue:
|
||||
self.queue[objects[0]._meta.model.__name__] = [] # noqa: SLF001
|
||||
|
||||
def push(self, obj):
|
||||
"""Add a model instance to queue to that it gets created in bulk."""
|
||||
"""Add a model instance to queue so that it gets created in bulk."""
|
||||
objects = self.queue[obj._meta.model.__name__] # noqa: SLF001
|
||||
objects.append(obj)
|
||||
if len(objects) > self.BATCH_SIZE:
|
||||
@@ -139,17 +152,19 @@ def create_demo(stdout):
|
||||
# pylint: disable=protected-access
|
||||
key = models.Document._int2str(i) # noqa: SLF001
|
||||
padding = models.Document.alphabet[0] * (models.Document.steplen - len(key))
|
||||
queue.push(
|
||||
models.Document(
|
||||
depth=1,
|
||||
path=f"{padding}{key}",
|
||||
creator_id=random.choice(users_ids),
|
||||
title=fake.sentence(nb_words=4),
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED
|
||||
if random_true_with_probability(0.5)
|
||||
else random.choice(models.LinkReachChoices.values),
|
||||
)
|
||||
title = fake.sentence(nb_words=4)
|
||||
document = models.Document(
|
||||
id=uuid4(),
|
||||
depth=1,
|
||||
path=f"{padding}{key}",
|
||||
creator_id=random.choice(users_ids),
|
||||
title=title,
|
||||
link_reach=models.LinkReachChoices.AUTHENTICATED
|
||||
if random_true_with_probability(0.5)
|
||||
else random.choice(models.LinkReachChoices.values),
|
||||
)
|
||||
document.save_content(get_ydoc_for_text(f"Content for {title:s}"))
|
||||
queue.push(document)
|
||||
|
||||
queue.flush()
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import sentry_sdk
|
||||
from configurations import Configuration, values
|
||||
from cryptography.fernet import Fernet
|
||||
from csp.constants import NONE
|
||||
from lasuite.configuration.values import SecretFileValue
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
@@ -99,6 +100,28 @@ class Base(Configuration):
|
||||
}
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
# Search
|
||||
SEARCH_INDEXER_CLASS = values.Value(
|
||||
default=None,
|
||||
environ_name="SEARCH_INDEXER_CLASS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
SEARCH_INDEXER_BATCH_SIZE = values.IntegerValue(
|
||||
default=100_000, environ_name="SEARCH_INDEXER_BATCH_SIZE", environ_prefix=None
|
||||
)
|
||||
SEARCH_INDEXER_URL = values.Value(
|
||||
default=None, environ_name="SEARCH_INDEXER_URL", environ_prefix=None
|
||||
)
|
||||
SEARCH_INDEXER_COUNTDOWN = values.IntegerValue(
|
||||
default=1, environ_name="SEARCH_INDEXER_COUNTDOWN", environ_prefix=None
|
||||
)
|
||||
SEARCH_INDEXER_SECRET = values.Value(
|
||||
default=None, environ_name="SEARCH_INDEXER_SECRET", environ_prefix=None
|
||||
)
|
||||
SEARCH_INDEXER_QUERY_URL = values.Value(
|
||||
default=None, environ_name="SEARCH_INDEXER_QUERY_URL", environ_prefix=None
|
||||
)
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.join(DATA_DIR, "static")
|
||||
@@ -490,7 +513,7 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue(
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value(
|
||||
60 * 60 * 24,
|
||||
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
@@ -922,6 +945,14 @@ class Development(Base):
|
||||
},
|
||||
}
|
||||
|
||||
# There is no key for token storage in default configuration.
|
||||
# In development environment we can create one if needed.
|
||||
OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
|
||||
default=Fernet.generate_key().decode(),
|
||||
environ_name="OIDC_STORE_REFRESH_TOKEN_KEY",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
# pylint: disable=invalid-name
|
||||
self.INSTALLED_APPS += ["django_extensions", "drf_spectacular_sidecar"]
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Breton\n"
|
||||
"Language: br_FR\n"
|
||||
@@ -50,36 +50,27 @@ msgstr "Kuzhet"
|
||||
msgid "Favorite"
|
||||
msgstr "Sinedoù"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr "Ar vaezienn-mañ a zo rekis."
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Korf"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Doare korf"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Stumm"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "eilenn {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr "implijer"
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Restr"
|
||||
msgid "Documents"
|
||||
msgstr "Restroù"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Restr hep titl"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Roud liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Roudoù liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Restr muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Restroù muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Liamm restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Liammoù restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr "Goulenn tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Goulennoù tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "deskrivadur"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "kod"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "publik"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Patrom"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Patromoù"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Liamm patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "Liammoù patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: 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:1362 core/models.py:1362
|
||||
#: 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:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "postel"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Pedadenn d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Pedadennoù d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo ar postel"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Digeriñ"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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. "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favorit"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Inhalt"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Typ"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "Kopie von {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr "Benutzer"
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: 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:1037 core/models.py:1037
|
||||
#: 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:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "Code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: 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:1362 core/models.py:1362
|
||||
#: 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:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo-E-Mail"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -234,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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 ""
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "¡Un nuevo documento se ha creado por ti!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Cuerpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Tipo de Cuerpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia de {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr "usuario"
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento sin título"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "¡{name} ha compartido un documento contigo!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: 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:966 core/models.py:966
|
||||
#: 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:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: 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:1037 core/models.py:1037
|
||||
#: 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:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr "Solicitud de acceso"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Solicitud de accesos"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} desea acceso al siguiente documento:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: 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} está pidiendo acceso al documento: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "código"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "público"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Plantillas"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relación plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relaciones plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: 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:1362 core/models.py:1362
|
||||
#: 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:1439 core/models.py:1439
|
||||
#: 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:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo de correo electrónico"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Abrir"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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, su nueva herramienta esencial para organizar, compartir y colaborar en sus documentos como equipo."
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -50,36 +50,27 @@ msgstr "Masqué"
|
||||
msgid "Favorite"
|
||||
msgstr "Favoris"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nouveau document a été créé pour vous !"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr "Ce champ est obligatoire."
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Corps"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Type de corps"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copie de {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr "utilisateur"
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant :"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} a partagé un document avec vous : {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: 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:966 core/models.py:966
|
||||
#: 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:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: 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:1037 core/models.py:1037
|
||||
#: 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:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: 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:1190 core/models.py:1190
|
||||
#: 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:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "public"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Modèles"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relation modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relations modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: 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:1362 core/models.py:1362
|
||||
#: 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:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo de l'e-mail"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Ouvrir"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Preferiti"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nuovo documento è stato creato a tuo nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sei ora proprietario di un nuovo documento:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia di {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr "utente"
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento senza titolo"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ha condiviso un documento con te!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha condiviso un documento con te: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: 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:1037 core/models.py:1037
|
||||
#: 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:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "pubblico"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Modello"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Modelli"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: 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:1362 core/models.py:1362
|
||||
#: 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:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo e-mail"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Apri"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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 ""
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -44,42 +44,33 @@ msgstr "Ik ben Eigenaar"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr "Gemaskeerd"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriete"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Een nieuw document was gecreëerd voor u!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "U heeft eigenaarschap van een nieuw document:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr "Dit veld is verplicht."
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Text"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Text type"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Formaat"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "kopie van {title}"
|
||||
@@ -172,7 +163,7 @@ msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
msgstr "Vereist. Minder dan 255 ASCII tekens."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "full name"
|
||||
@@ -234,8 +225,8 @@ msgstr "gebruiker"
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} heeft een document met u gedeeld: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: 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:1037 core/models.py:1037
|
||||
#: 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:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr "Document verzoekt om toegang"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Document verzoekt om toegangen"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} wenst toegang tot een document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} wenst toegang tot het volgende document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: 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} vraagt toegang tot het document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "omschrijving"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "publiek"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Template"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Templates"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Template/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "Template/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: 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:1362 core/models.py:1362
|
||||
#: 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:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "email adres"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo email"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Open"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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, jouw nieuwe essentiële tool voor het organiseren, delen en collaboreren van documenten als team. "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Um novo documento foi criado em seu nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "A propriedade de um novo documento foi concedida a você:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Tipo de corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "cópia de {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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 ""
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
@@ -50,36 +50,27 @@ msgstr "Скрытый"
|
||||
msgid "Favorite"
|
||||
msgstr "Избранное"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Новый документ был создан от вашего имени!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Вы назначены владельцем для нового документа:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr "Это поле обязательное."
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Текст сообщения"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Тип сообщения"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копия {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr "пользователь"
|
||||
msgid "users"
|
||||
msgstr "пользователи"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документы"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Безымянный документ"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} делится с вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} делится с вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трассировка связи документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трассировка связей документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Избранный документ"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Избранные документы"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Отношение документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Отношения документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Эта команда уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr "Документ запрашивает доступ"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Документ запрашивает доступы"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хочет получить доступ к документу!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} хочет получить доступ к следующему документу:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запрашивает доступ к документу: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "описание"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "доступно всем"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Шаблоны"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Отношение шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "Отношения шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Этот пользователь уже указан в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Эта команда уже указана в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Приглашение для документа"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Приглашения для документов"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Логотип email"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Открыть"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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, ваш новый инструмент для организации и совместного использования документов в вашей команде. "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Language: sl_SI\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Priljubljena"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Telo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Vrsta telesa"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Oblika"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -234,8 +225,8 @@ msgstr "uporabnik"
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Dokument brez naslova"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} je delil dokument z vami!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} je delil dokument z vami: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: 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:966 core/models.py:966
|
||||
#: 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:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: 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:1037 core/models.py:1037
|
||||
#: 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:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "koda"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "javno"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Predloga"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Predloge"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Odnos predloga/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: 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:1356 core/models.py:1356
|
||||
#: 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:1362 core/models.py:1362
|
||||
#: 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:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "E-pošta z logotipom"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Odpri"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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 " Dokumenti, vaše novo bistveno orodje za organiziranje, skupno rabo in skupinsko sodelovanje pri dokumentih. "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Language: sv_SE\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriter"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ett nytt dokument skapades åt dig!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -234,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: 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."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logotyp e-post"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öppna"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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 ""
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -50,36 +50,27 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -234,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: 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:1265 core/models.py:1265
|
||||
#: 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:1271 core/models.py:1271
|
||||
#: 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:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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 ""
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
@@ -50,36 +50,27 @@ msgstr "Приховано"
|
||||
msgid "Favorite"
|
||||
msgstr "Обране"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Новий документ був створений від вашого імені!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Ви тепер є власником нового документа:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr "Це поле є обов’язковим."
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "Вміст"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "Тип вмісту"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копія {title}"
|
||||
@@ -234,8 +225,8 @@ msgstr "користувач"
|
||||
msgid "users"
|
||||
msgstr "користувачі"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документи"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "Документ без назви"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ділиться з вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ділиться з вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трасування посилання Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трасування посилань Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "Обраний документ"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "Обрані документи"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Цей користувач вже має доступ до цього документу."
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ця команда вже має доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr "Запит доступу до документа"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Запит доступу для документа"
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: 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:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хоче отримати доступ до документа!"
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} бажає отримати доступ до наступного документа:"
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запитує доступ до документа: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "опис"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "публічне"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "Шаблони"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Цей користувач вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ця команда вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "електронна адреса"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "Запрошення до редагування документа"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "Запрошення до редагування документів"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Логотип пошти"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Відкрити"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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, ваш новий важливий інструмент для організації, обміну та командної співпраці над вашими документами. "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -44,42 +44,33 @@ msgstr "创建者是我"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr "已屏蔽"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "收藏"
|
||||
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "已为您创建了一份新文档!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "您已被授予新文档的所有权:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||
msgid "This field is required."
|
||||
msgstr "必填字段。"
|
||||
|
||||
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||
#, python-format
|
||||
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
msgid "Body"
|
||||
msgstr "正文"
|
||||
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
msgid "Body type"
|
||||
msgstr "正文类型"
|
||||
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
msgid "Format"
|
||||
msgstr "格式"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "{title} 的副本"
|
||||
@@ -172,7 +163,7 @@ msgstr "sub"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "full name"
|
||||
@@ -234,8 +225,8 @@ msgstr "用户"
|
||||
msgid "users"
|
||||
msgstr "个用户"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
msgid "title"
|
||||
msgstr "标题"
|
||||
|
||||
@@ -251,174 +242,174 @@ msgstr "文档"
|
||||
msgid "Documents"
|
||||
msgstr "个文档"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
msgid "Untitled Document"
|
||||
msgstr "未命名文档"
|
||||
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} 与您共享了一个文档!"
|
||||
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} 与您共享了一个文档:{title}"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link traces"
|
||||
msgstr "个文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
#: 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:995 core/models.py:995
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
msgid "Document favorite"
|
||||
msgstr "文档收藏"
|
||||
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorites"
|
||||
msgstr "文档收藏夹"
|
||||
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
#: 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:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "Document/user relation"
|
||||
msgstr "文档/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relations"
|
||||
msgstr "文档/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
msgid "This user is already in this document."
|
||||
msgstr "该用户已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
msgid "This team is already in this document."
|
||||
msgstr "该团队已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
#: 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:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr "文档需要访问权限"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "文档需要访问权限"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "用户已申请该文档的访问权限。"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} 申请访问文档!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} 申请访问以下文档:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name}申请文档:{title}的访问权限"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
msgid "description"
|
||||
msgstr "说明"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "code"
|
||||
msgstr "代码"
|
||||
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
#: 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:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
msgid "Template"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Templates"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
msgid "Template/user relation"
|
||||
msgstr "模板/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relations"
|
||||
msgstr "模板/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
msgid "This user is already in this template."
|
||||
msgstr "该用户已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
msgid "This team is already in this template."
|
||||
msgstr "该团队已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
msgid "email address"
|
||||
msgstr "电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
msgid "Document invitation"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitations"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "此电子邮件已经与现有注册用户关联。"
|
||||
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "徽标邮件"
|
||||
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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——您的全新必备工具,帮助团队组织、共享和协作处理文档。 "
|
||||
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "3.8.2"
|
||||
version = "3.7.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -39,7 +39,7 @@ dependencies = [
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.7",
|
||||
"django==5.2.6",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable playwright/no-conditional-expect */
|
||||
import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { chromium, expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
|
||||
import {
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
|
||||
import { createRootSubPage, navigateToPageFromTree } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -561,7 +560,20 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await updateShareLink(page, 'Public', 'Editing');
|
||||
await page.getByTestId('doc-visibility').click();
|
||||
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'Public',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId('doc-access-mode').click();
|
||||
await page.getByRole('menuitem', { name: 'Editing' }).click();
|
||||
|
||||
// Close the modal
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
@@ -585,12 +597,17 @@ test.describe('Doc Editor', () => {
|
||||
* We open another browser that will connect to the collaborative server
|
||||
* and will block the current browser to edit the doc.
|
||||
*/
|
||||
const { otherPage } = await connectOtherUserToDoc({
|
||||
browserName,
|
||||
docUrl: urlChildDoc,
|
||||
docTitle: childTitle,
|
||||
withoutSignIn: true,
|
||||
const otherBrowser = await chromium.launch({ headless: true });
|
||||
const otherContext = await otherBrowser.newContext({
|
||||
locale: 'en-US',
|
||||
timezoneId: 'Europe/Paris',
|
||||
permissions: [],
|
||||
storageState: {
|
||||
cookies: [],
|
||||
origins: [],
|
||||
},
|
||||
});
|
||||
const otherPage = await otherContext.newPage();
|
||||
|
||||
const webSocketPromise = otherPage.waitForEvent(
|
||||
'websocket',
|
||||
@@ -631,11 +648,6 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await expect(editor).toHaveAttribute('contenteditable', 'false');
|
||||
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).toBeHidden();
|
||||
await expect(page.getByRole('heading', { name: childTitle })).toBeVisible();
|
||||
|
||||
await page.goto(urlParentDoc);
|
||||
|
||||
await verifyDocName(page, parentTitle);
|
||||
@@ -652,11 +664,6 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await expect(editor).toHaveAttribute('contenteditable', 'true');
|
||||
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).toContainText(childTitle);
|
||||
await expect(page.getByRole('heading', { name: childTitle })).toBeHidden();
|
||||
|
||||
await expect(
|
||||
card.getByText('Others are editing. Your network prevent changes.'),
|
||||
).toBeHidden();
|
||||
@@ -760,7 +767,7 @@ test.describe('Doc Editor', () => {
|
||||
// Wait for the interlink to be created and rendered
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
|
||||
const interlink = editor.getByRole('button', {
|
||||
const interlink = editor.getByRole('link', {
|
||||
name: docChild2,
|
||||
});
|
||||
|
||||
@@ -846,10 +853,10 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await page.getByText('Add PDF').click();
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByText('Upload file').click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
|
||||
console.log(path.join(__dirname, 'assets/test-pdf.pdf'));
|
||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf'));
|
||||
|
||||
// Wait for the media-check to be processed
|
||||
@@ -866,12 +873,5 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
await expect(pdfEmbed).toHaveAttribute('type', 'application/pdf');
|
||||
await expect(pdfEmbed).toHaveAttribute('role', 'presentation');
|
||||
|
||||
// Check download with original filename
|
||||
await page.locator('.bn-block-content[data-content-type="pdf"]').click();
|
||||
await page.locator('[data-test="downloadfile"]').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe('test-pdf.pdf');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
verifyDocName,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -154,13 +153,11 @@ test.describe('Doc Export', () => {
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await writeInEditor({
|
||||
page,
|
||||
text: 'Hello World 😃🎉🚀🙋♀️🧑🏿❤️💋🧑🏾',
|
||||
});
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await openSuggestionMenu({ page });
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Resizable image with caption').click();
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
@@ -472,7 +469,7 @@ test.describe('Doc Export', () => {
|
||||
|
||||
// Search the interlinking link in the editor (not in the document tree)
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
const interlink = editor.getByRole('button', {
|
||||
const interlink = editor.getByRole('link', {
|
||||
name: randomDoc,
|
||||
});
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ test.describe('Doc grid dnd', () => {
|
||||
|
||||
await expect(dragOverlay).toBeVisible();
|
||||
await expect(dragOverlay).toHaveText(
|
||||
'You must be at least the administrator of the target document',
|
||||
'You must be at least the editor of the target document',
|
||||
);
|
||||
|
||||
await page.mouse.up();
|
||||
|
||||
@@ -139,7 +139,7 @@ test.describe('Document grid item options', () => {
|
||||
const row = await getGridRow(page, docTitle);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
|
||||
@@ -96,7 +96,7 @@ test.describe('Doc Header', () => {
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText(`This document will be`)).toBeVisible();
|
||||
await expect(page.getByText(`This document and any sub-`)).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -142,10 +142,6 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).toContainText('Mocked document');
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export the document' }),
|
||||
).toBeVisible();
|
||||
@@ -222,10 +218,6 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'Document title' }),
|
||||
).toContainText('Mocked document');
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export the document' }),
|
||||
).toBeVisible();
|
||||
@@ -294,10 +286,6 @@ test.describe('Doc Header', () => {
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Mocked document' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export the document' }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -169,15 +169,8 @@ test.describe('Document create member', () => {
|
||||
|
||||
const inputSearch = page.getByTestId('quick-search-input');
|
||||
|
||||
let email = 'user.test21@example.COM';
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
await inputSearch.fill(email);
|
||||
|
||||
// Check email is found in search (case insensitive)
|
||||
await expect(page.getByRole('option').getByText(email)).toHaveCount(1);
|
||||
|
||||
email = email + 'M';
|
||||
await inputSearch.fill(email);
|
||||
|
||||
await page.getByTestId(`search-user-row-${email}`).click();
|
||||
|
||||
// Choose a role
|
||||
@@ -198,23 +191,13 @@ test.describe('Document create member', () => {
|
||||
|
||||
const listInvitation = page.getByTestId('doc-share-quick-search');
|
||||
const userInvitation = listInvitation.getByTestId(
|
||||
`doc-share-invitation-row-${email.toLowerCase()}`,
|
||||
`doc-share-invitation-row-${email}`,
|
||||
);
|
||||
await expect(userInvitation).toBeVisible();
|
||||
|
||||
const responsePromisePatchInvitation = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/invitations/') &&
|
||||
response.status() === 200 &&
|
||||
response.request().method() === 'PATCH',
|
||||
);
|
||||
|
||||
await userInvitation.getByLabel('doc-role-dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Reader' }).click();
|
||||
|
||||
const responsePatchInvitation = await responsePromisePatchInvitation;
|
||||
expect(responsePatchInvitation.ok()).toBeTruthy();
|
||||
|
||||
const moreActions = userInvitation.getByRole('button', {
|
||||
name: 'Open invitation actions menu',
|
||||
});
|
||||
@@ -246,11 +229,11 @@ test.describe('Document create member', () => {
|
||||
.last()
|
||||
.fill('Hello World');
|
||||
|
||||
const docUrl = page.url();
|
||||
const urlDoc = page.url();
|
||||
|
||||
// Other user will request access
|
||||
const { otherPage, otherBrowserName, cleanup } =
|
||||
await connectOtherUserToDoc({ browserName, docUrl });
|
||||
await connectOtherUserToDoc(browserName, urlDoc);
|
||||
|
||||
await expect(
|
||||
otherPage.getByText('Insufficient access rights to view the document.'),
|
||||
|
||||
@@ -60,30 +60,35 @@ test.describe('Doc Routing', () => {
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
const responsePromise = page.route(
|
||||
/.*\/documents\/.*\/$|users\/me\/$/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
// Wait for the doc link (via its dynamic title) to be visible
|
||||
const docLink = page.getByRole('link', { name: docTitle });
|
||||
await expect(docLink).toBeVisible();
|
||||
|
||||
if (
|
||||
request.method().includes('PATCH') ||
|
||||
request.method().includes('GET')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
json: {
|
||||
detail: 'Log in to access the document',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
},
|
||||
// Intercept GET/PATCH requests to return 401
|
||||
await page.route(/.*\/documents\/.*\/$|users\/me\/$/, async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('PATCH') ||
|
||||
request.method().includes('GET')
|
||||
) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
json: { detail: 'Log in to access the document' },
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Explicitly wait for a 401 response after clicking
|
||||
const wait401 = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.status() === 401 &&
|
||||
/\/(documents\/[^/]+\/|users\/me\/)$/.test(resp.url()),
|
||||
);
|
||||
|
||||
await page.getByRole('link', { name: '401-doc-parent' }).click();
|
||||
|
||||
await responsePromise;
|
||||
await docLink.click();
|
||||
await wait401;
|
||||
|
||||
await expect(page.getByText('Log in to access the document.')).toBeVisible({
|
||||
timeout: 10000,
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import {
|
||||
clickInEditorMenu,
|
||||
clickInGridMenu,
|
||||
createDoc,
|
||||
getGridRow,
|
||||
verifyDocName,
|
||||
} from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
import {
|
||||
addChild,
|
||||
createRootSubPage,
|
||||
navigateToPageFromTree,
|
||||
} from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Trashbin', () => {
|
||||
test('it controls UI and interaction from the grid page', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [title1] = await createDoc(page, 'my-trash-doc-1', browserName, 1);
|
||||
const [title2] = await createDoc(page, 'my-trash-doc-2', browserName, 1);
|
||||
await verifyDocName(page, title2);
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await addNewMember(page, 0, 'Editor');
|
||||
await page.getByRole('button', { name: 'close' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||
|
||||
const row1 = await getGridRow(page, title1);
|
||||
await clickInGridMenu(page, row1, 'Delete');
|
||||
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||
await expect(row1.getByText(title1)).toBeHidden();
|
||||
|
||||
const row2 = await getGridRow(page, title2);
|
||||
await clickInGridMenu(page, row2, 'Delete');
|
||||
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||
await expect(row2.getByText(title2)).toBeHidden();
|
||||
|
||||
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid.getByText('Days remaining')).toBeVisible();
|
||||
await expect(row1.getByText(title1)).toBeVisible();
|
||||
await expect(row1.getByText('30 days')).toBeVisible();
|
||||
await expect(row2.getByText(title2)).toBeVisible();
|
||||
await expect(
|
||||
row2.getByRole('button', {
|
||||
name: 'Open the sharing settings for the document',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
row2.getByRole('button', {
|
||||
name: 'Open the sharing settings for the document',
|
||||
}),
|
||||
).toBeDisabled();
|
||||
|
||||
await clickInGridMenu(page, row2, 'Restore');
|
||||
|
||||
await expect(row2.getByText(title2)).toBeHidden();
|
||||
await page.getByRole('link', { name: 'All docs' }).click();
|
||||
const row2Restored = await getGridRow(page, title2);
|
||||
await expect(row2Restored.getByText(title2)).toBeVisible();
|
||||
await row2Restored.getByRole('link', { name: /Open document/ }).click();
|
||||
|
||||
await verifyDocName(page, title2);
|
||||
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||
await expect(row2.getByText(title2)).toBeVisible();
|
||||
await expect(
|
||||
row2.getByRole('button', {
|
||||
name: 'Open the sharing settings for the document',
|
||||
}),
|
||||
).toBeEnabled();
|
||||
|
||||
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||
await expect(row2.getByText(title2)).toBeHidden();
|
||||
});
|
||||
|
||||
test('it controls UI and interaction from the doc page', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [topParent] = await createDoc(
|
||||
page,
|
||||
'my-trash-editor-doc',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, topParent);
|
||||
const { name: subDocName } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'my-trash-editor-subdoc',
|
||||
);
|
||||
|
||||
const subsubDocName = await addChild({
|
||||
page,
|
||||
browserName,
|
||||
docParent: subDocName,
|
||||
});
|
||||
await verifyDocName(page, subsubDocName);
|
||||
|
||||
await navigateToPageFromTree({ page, title: subDocName });
|
||||
await verifyDocName(page, subDocName);
|
||||
|
||||
await clickInEditorMenu(page, 'Delete sub-document');
|
||||
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||
await verifyDocName(page, topParent);
|
||||
|
||||
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||
const row = await getGridRow(page, subDocName);
|
||||
await row.getByText(subDocName).click();
|
||||
await verifyDocName(page, subDocName);
|
||||
|
||||
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
|
||||
await expect(page.locator('.bn-editor')).toHaveAttribute(
|
||||
'contenteditable',
|
||||
'false',
|
||||
);
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
await expect(docTree.getByText(topParent)).toBeHidden();
|
||||
await expect(
|
||||
docTree.getByText(subDocName, {
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(docTree.getByText(subsubDocName)).toBeVisible();
|
||||
await expect(
|
||||
docTree
|
||||
.locator(".--docs-sub-page-item[aria-disabled='true']")
|
||||
.getByText(subsubDocName),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Restore' }).click();
|
||||
await expect(page.getByLabel('Alert deleted document')).toBeHidden();
|
||||
await expect(page.locator('.bn-editor')).toHaveAttribute(
|
||||
'contenteditable',
|
||||
'true',
|
||||
);
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeEnabled();
|
||||
await expect(docTree.getByText(topParent)).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -157,12 +157,12 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
.last()
|
||||
.fill('Hello World');
|
||||
|
||||
const docUrl = page.url();
|
||||
const urlDoc = page.url();
|
||||
|
||||
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
|
||||
const { otherBrowserName, otherPage } = await connectOtherUserToDoc(
|
||||
browserName,
|
||||
docUrl,
|
||||
});
|
||||
urlDoc,
|
||||
);
|
||||
|
||||
await expect(
|
||||
otherPage.getByText('Insufficient access rights to view the document.'),
|
||||
|
||||
@@ -32,15 +32,6 @@ test.describe('Header', () => {
|
||||
}) => {
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
theme_customization: {
|
||||
header: {
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
width: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
@@ -107,15 +98,6 @@ test.describe('Header mobile', () => {
|
||||
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
theme_customization: {
|
||||
header: {
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
width: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
@@ -149,27 +131,3 @@ test.describe('Header: Log out', () => {
|
||||
await expectLoginPage(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Header: Override configuration', () => {
|
||||
test('checks the header is correctly overrided', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
theme_customization: {
|
||||
header: {
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
width: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
|
||||
await expect(header.getByAltText('Docs')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page, expect } from '@playwright/test';
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
|
||||
@@ -23,7 +23,6 @@ export const CONFIG = {
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
POSTHOG_KEY: {},
|
||||
SENTRY_DSN: null,
|
||||
TRASHBIN_CUTOFF_DAYS: 30,
|
||||
theme_customization: {},
|
||||
} as const;
|
||||
|
||||
@@ -326,19 +325,3 @@ export async function waitForLanguageSwitch(
|
||||
|
||||
await page.getByRole('menuitem', { name: lang.label }).click();
|
||||
}
|
||||
|
||||
export const clickInEditorMenu = async (page: Page, textButton: string) => {
|
||||
await page.getByRole('button', { name: 'Open the document options' }).click();
|
||||
await page.getByRole('menuitem', { name: textButton }).click();
|
||||
};
|
||||
|
||||
export const clickInGridMenu = async (
|
||||
page: Page,
|
||||
row: Locator,
|
||||
textButton: string,
|
||||
) => {
|
||||
await row
|
||||
.getByRole('button', { name: /Open the menu of actions for the document/ })
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: textButton }).click();
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
export type Role = 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader';
|
||||
export type LinkReach = 'Private' | 'Connected' | 'Public';
|
||||
export type LinkRole = 'Reading' | 'Editing';
|
||||
export type LinkRole = 'Reading' | 'Edition';
|
||||
|
||||
export const addNewMember = async (
|
||||
page: Page,
|
||||
@@ -88,17 +88,11 @@ export const updateRoleUser = async (
|
||||
* @param docTitle The title of the document (optional).
|
||||
* @returns An object containing the other browser, context, and page.
|
||||
*/
|
||||
export const connectOtherUserToDoc = async ({
|
||||
browserName,
|
||||
docUrl,
|
||||
docTitle,
|
||||
withoutSignIn,
|
||||
}: {
|
||||
browserName: BrowserName;
|
||||
docUrl: string;
|
||||
docTitle?: string;
|
||||
withoutSignIn?: boolean;
|
||||
}) => {
|
||||
export const connectOtherUserToDoc = async (
|
||||
browserName: BrowserName,
|
||||
docUrl: string,
|
||||
docTitle?: string,
|
||||
) => {
|
||||
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
|
||||
if (!otherBrowserName) {
|
||||
throw new Error('No alternative browser found');
|
||||
@@ -117,16 +111,15 @@ export const connectOtherUserToDoc = async ({
|
||||
const otherPage = await otherContext.newPage();
|
||||
await otherPage.goto(docUrl);
|
||||
|
||||
if (!withoutSignIn) {
|
||||
await otherPage
|
||||
.getByRole('main', { name: 'Main content' })
|
||||
.getByLabel('Login')
|
||||
.click({
|
||||
timeout: 15000,
|
||||
});
|
||||
await otherPage
|
||||
.getByRole('main', { name: 'Main content' })
|
||||
.getByLabel('Login')
|
||||
.click({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
||||
|
||||
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
||||
}
|
||||
if (docTitle) {
|
||||
await verifyDocName(otherPage, docTitle);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
BrowserName,
|
||||
randomName,
|
||||
updateDocTitle,
|
||||
verifyDocName,
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
|
||||
export const createRootSubPage = async (
|
||||
page: Page,
|
||||
browserName: BrowserName,
|
||||
browserName: string,
|
||||
docName: string,
|
||||
isMobile: boolean = false,
|
||||
) => {
|
||||
@@ -68,47 +67,6 @@ export const clickOnAddRootSubPage = async (page: Page) => {
|
||||
await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
|
||||
};
|
||||
|
||||
export const addChild = async ({
|
||||
page,
|
||||
browserName,
|
||||
docParent,
|
||||
}: {
|
||||
page: Page;
|
||||
browserName: BrowserName;
|
||||
docParent: string;
|
||||
}) => {
|
||||
let item = page.getByTestId('doc-tree-root-item');
|
||||
|
||||
const isParent = await item
|
||||
.filter({
|
||||
hasText: docParent,
|
||||
})
|
||||
.first()
|
||||
.count();
|
||||
|
||||
if (!isParent) {
|
||||
const items = page.getByRole('treeitem');
|
||||
|
||||
item = items
|
||||
.filter({
|
||||
hasText: docParent,
|
||||
})
|
||||
.first();
|
||||
}
|
||||
|
||||
await item.hover();
|
||||
await item.getByTestId('doc-tree-item-actions-add-child').click();
|
||||
|
||||
const [name] = randomName(docParent, browserName, 1);
|
||||
await updateDocTitle(page, name);
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
|
||||
await page.getByRole('link', { name: /Open root document/ }).click();
|
||||
};
|
||||
|
||||
export const navigateToPageFromTree = async ({
|
||||
page,
|
||||
title,
|
||||
@@ -117,10 +75,6 @@ export const navigateToPageFromTree = async ({
|
||||
title: string;
|
||||
}) => {
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
await docTree
|
||||
.getByText(title, {
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
await docTree.getByText(title).click();
|
||||
await verifyDocName(page, title);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "3.8.2",
|
||||
"version": "3.7.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
|
||||
5
src/frontend/apps/impress/.gitignore
vendored
5
src/frontend/apps/impress/.gitignore
vendored
@@ -35,8 +35,3 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
|
||||
service-worker.js
|
||||
|
||||
# Font embedding
|
||||
public/assets/fonts/emoji/*
|
||||
!public/assets/fonts/emoji/fallback.png
|
||||
public/assets/fonts/Marianne/*
|
||||
@@ -1,3 +1,2 @@
|
||||
next-env.d.ts
|
||||
service-worker.js
|
||||
public/assets/fonts/*
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
out/**/*
|
||||
public/assets/fonts/**/*
|
||||
node_modules/**/*
|
||||
@@ -1,7 +1,5 @@
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||
|
||||
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
|
||||
@@ -43,30 +41,6 @@ const nextConfig = {
|
||||
},
|
||||
);
|
||||
|
||||
// Copy necessary fonts from node_modules to public directory during build or dev
|
||||
config.plugins.push(
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(
|
||||
__dirname,
|
||||
'../../node_modules/emoji-datasource-apple/img/apple/64',
|
||||
),
|
||||
to: path.resolve(__dirname, 'public/assets/fonts/emoji'),
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
from: path.resolve(
|
||||
__dirname,
|
||||
'../../node_modules/@gouvfr-lasuite/ui-kit/dist/assets/fonts/Marianne',
|
||||
),
|
||||
to: path.resolve(__dirname, 'public/assets/fonts/Marianne'),
|
||||
force: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isServer && process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true') {
|
||||
config.plugins.push(
|
||||
new InjectManifest({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "3.8.2",
|
||||
"version": "3.7.0",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
@@ -30,8 +30,6 @@
|
||||
"@dnd-kit/modifiers": "9.0.0",
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@fontsource-variable/inter": "5.2.8",
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.25",
|
||||
"@fontsource/material-icons": "5.2.5",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.16.1",
|
||||
@@ -45,7 +43,6 @@
|
||||
"cmdk": "1.1.1",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"docx": "9.5.0",
|
||||
"emoji-datasource-apple": "16.0.0",
|
||||
"emoji-mart": "5.6.0",
|
||||
"emoji-regex": "10.5.0",
|
||||
"i18next": "25.5.2",
|
||||
@@ -80,7 +77,6 @@
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"@vitejs/plugin-react": "5.0.2",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"cross-env": "10.0.0",
|
||||
"dotenv": "17.2.2",
|
||||
"eslint-plugin-docs": "*",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,101 @@
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Thin.woff2') format('woff2'),
|
||||
url('Marianne-Thin.woff') format('woff');
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Thin_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Thin_Italic.woff') format('woff');
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Light.woff2') format('woff2'),
|
||||
url('Marianne-Light.woff') format('woff');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Light_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Light_Italic.woff') format('woff');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Regular.woff2') format('woff2'),
|
||||
url('Marianne-Regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Regular_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Regular_Italic.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Medium.woff2') format('woff2'),
|
||||
url('Marianne-Medium.woff') format('woff');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Medium_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Medium_Italic.woff') format('woff');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Bold.woff2') format('woff2'),
|
||||
url('Marianne-Bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-Bold_Italic.woff2') format('woff2'),
|
||||
url('Marianne-Bold_Italic.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-ExtraBold.woff2') format('woff2'),
|
||||
url('Marianne-ExtraBold.woff') format('woff');
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Marianne;
|
||||
src:
|
||||
url('Marianne-ExtraBold_Italic.woff2') format('woff2'),
|
||||
url('Marianne-ExtraBold_Italic.woff') format('woff');
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -36,30 +36,7 @@ export interface BoxProps {
|
||||
$position?: CSSProperties['position'];
|
||||
$radius?: CSSProperties['borderRadius'];
|
||||
$shrink?: CSSProperties['flexShrink'];
|
||||
$theme?:
|
||||
| 'primary'
|
||||
| 'primary-text'
|
||||
| 'secondary'
|
||||
| 'secondary-text'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'greyscale';
|
||||
$transition?: CSSProperties['transition'];
|
||||
$variation?:
|
||||
| 'text'
|
||||
| '000'
|
||||
| '100'
|
||||
| '200'
|
||||
| '300'
|
||||
| '400'
|
||||
| '500'
|
||||
| '600'
|
||||
| '700'
|
||||
| '800'
|
||||
| '900'
|
||||
| '1000';
|
||||
$width?: CSSProperties['width'];
|
||||
$wrap?: CSSProperties['flexWrap'];
|
||||
$zIndex?: CSSProperties['zIndex'];
|
||||
@@ -96,12 +73,6 @@ export const Box = styled('div')<BoxProps>`
|
||||
${({ $position }) => $position && `position: ${$position};`}
|
||||
${({ $radius }) => $radius && `border-radius: ${$radius};`}
|
||||
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
|
||||
${({ $theme, $variation }) => {
|
||||
if (!$theme || !$variation) {
|
||||
return '';
|
||||
}
|
||||
return `color: var(--c--theme--colors--${$theme}-${$variation});`;
|
||||
}}
|
||||
${({ $transition }) => $transition && `transition: ${$transition};`}
|
||||
${({ $width }) => $width && `width: ${$width};`}
|
||||
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user