Compare commits

..

8 Commits
v3.8.1 ... anct

Author SHA1 Message Date
Sylvain Zimmer
1b200ef7e3 Merge remote-tracking branch 'origin/buildpack' into anct 2025-08-08 10:47:00 +02:00
Sylvain Zimmer
81076f18fe Merge branch 'api_content' into anct 2025-08-08 10:44:51 +02:00
Sylvain Zimmer
b7ffc766d8 🗑️(convert) cleanup old content route 2025-07-24 14:52:16 +02:00
Sylvain Zimmer
148890a295 (convert) improve tests with stricter tests and less ipsum 2025-07-24 14:49:20 +02:00
Sylvain Zimmer
ab05fa6557 ♻️(convert) reuse existing convert yprovider endpoint for content API 2025-07-24 14:08:18 +02:00
Sylvain Zimmer
cdc24114b6 🚨(lint) fix lint
Oops
2025-07-24 02:42:30 +02:00
Sylvain Zimmer
0bd53aed2c (api) add API route to fetch document content
This allows API users to process document content, enabling the
use of Docs as a headless CMS for instance, or any kind of document
processing. Fixes #1206.
2025-07-24 02:31:50 +02:00
Sylvain Zimmer
ddac6197e3 (buildpack) add PaaS deployment support, tested with Scalingo 2025-06-01 23:54:35 +02:00
333 changed files with 4619 additions and 12297 deletions

View File

@@ -23,10 +23,9 @@ jobs:
uses: actions/checkout@v4
# Backend i18n
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v3
with:
python-version: "3.13.3"
cache: "pip"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies

View File

@@ -31,11 +31,8 @@ jobs:
images: lasuite/impress-backend
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
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
uses: numerique-gouv/action-trivy-cache@main
@@ -49,7 +46,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,11 +64,8 @@ jobs:
images: lasuite/impress-frontend
-
name: Login to DockerHub
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
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
uses: numerique-gouv/action-trivy-cache@main
@@ -88,7 +82,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 +100,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 +116,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 +125,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

View File

@@ -101,7 +101,7 @@ jobs:
test-e2e-other-browser:
runs-on: ubuntu-latest
needs: test-e2e-chromium
timeout-minutes: 30
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -136,54 +136,3 @@ jobs:
name: playwright-other-report
path: src/frontend/apps/e2e/report/
retention-days: 7
bundle-size-check:
runs-on: ubuntu-latest
needs: install-dependencies
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Detect relevant changes
id: changes
uses: dorny/paths-filter@v2
with:
filters: |
lock:
- 'src/frontend/**/yarn.lock'
app:
- 'src/frontend/apps/impress/**'
- name: Restore the frontend cache
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
fail-on-cache-miss: true
- name: Setup Node.js
if: steps.changes.outputs.lock == 'true' || steps.changes.outputs.app == 'true'
uses: actions/setup-node@v4
with:
node-version: "22.x"
- name: Check bundle size changes
if: steps.changes.outputs.lock == 'true' || steps.changes.outputs.app == 'true'
uses: preactjs/compressed-size-action@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
build-script: "app:build"
pattern: "apps/impress/out/**/*.{css,js,html}"
exclude: "{**/*.map,**/node_modules/**}"
minimum-change-threshold: 500
compression: "gzip"
cwd: "./src/frontend"
show-total: true
strip-hash: "[-_.][a-f0-9]{8,}(?=\\.(?:js|css|html)$)"
omit-unchanged: true
install-script: "yarn install --frozen-lockfile"

View File

@@ -25,18 +25,14 @@ jobs:
- name: show
run: git log
- name: Enforce absence of print statements in code
if: always()
run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
- name: Check absence of fixup commits
if: always()
run: |
! git log | grep 'fixup!'
- name: Install gitlint
if: always()
run: pip install --user requests gitlint
- name: Lint commit messages added to main
if: always()
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
check-changelog:
@@ -79,7 +75,6 @@ jobs:
--check-filenames \
--ignore-words-list "Dokument,afterAll,excpt,statics" \
--skip "./git/" \
--skip "**/*.pdf" \
--skip "**/*.po" \
--skip "**/*.pot" \
--skip "**/*.json" \
@@ -94,10 +89,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v3
with:
python-version: "3.13.3"
cache: "pip"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
@@ -190,10 +184,9 @@ jobs:
mc version enable impress/impress-media-storage"
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v3
with:
python-version: "3.13.3"
cache: "pip"
- name: Install development dependencies
run: pip install --user .[dev]

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
@@ -6,132 +8,21 @@ and this project adheres to
## [Unreleased]
## [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
- ♿ 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
- ♿ hide decorative icons from assistive tech with aria-hidden #1404
- ♿ fix rgaa 1.9.1: convert to figure/figcaption structure #1426
- ♿ remove redundant aria-label to avoid over-accessibility #1420
- ♿ 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
- ♿ 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
- (frontend) improve accessibility:
- #1248
- #1235
- #1255
- #1262
- #1244
- #1270
### Fixed
- 🐛(backend) duplicate sub docs as root for reader users #1385
- ⚗️(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
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
- 🐛(minio) fix user permission error with Minio and Windows #1264
## [3.7.0] - 2025-09-12
### Added
- ✨(api) add API route to fetch document content #1206
### Changed
- 🔒️(backend) configure throttle on every viewsets #1343
- ⬆️ Bump eslint to V9 #1071
- ♿(frontend) improve accessibility:
- ♿fix major accessibility issues reported by wave and axe #1344
- ✨unify tab focus style for better visual consistency #1341
- ✨improve modal a11y: structure, labels, and title #1349
- ✨improve accessibility of cdoc content with correct aria tags #1271
- ✨unify tab focus style for better visual consistency #1341
- ♿hide decorative icons, label menus, avoid accessible name… #1362
- ♻️(tilt) use helm dev-backend chart
### Removed
- 🔥(frontend) remove multi column drop cursor #1370
### Fixed
- 🐛(frontend) fix callout emoji list #1366
## [3.6.0] - 2025-09-04
### Added
- 👷(CI) add bundle size check job #1268
- ✨(frontend) use title first emoji as doc icon in tree #1289
### Changed
- ♻️(docs-app) Switch from Jest tests to Vitest #1269
- ♿(frontend) improve accessibility:
- 🌐(frontend) set html lang attribute dynamically #1248
- ♿(frontend) inject language attribute to pdf export #1235
- ♿(frontend) improve accessibility of search modal #1275
- ♿(frontend) add correct attributes to icons #1255
- 🎨(frontend) improve nav structure #1262
- ♿️(frontend) keyboard interaction with menu #1244
- ♿(frontend) improve header accessibility #1270
- ♿(frontend) improve accessibility for decorative images in editor #1282
- #1338
- #1281
- ♻️(backend) fallback to email identifier when no name #1298
- 🐛(backend) allow ASCII characters in user sub field #1295
- ⚡️(frontend) improve fallback width calculation #1333
### Fixed
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1263
- 🐛(minio) fix user permission error with Minio and Windows #1263
- 🐛(frontend) fix export when quote block and inline code #1319
- 🐛(frontend) fix base64 font #1324
- 🐛(backend) allow creator to delete subpages #1297
- 🐛(frontend) fix dnd conflict with tree and Blocknote #1328
- 🐛(frontend) fix display bug on homepage #1332
- 🐛link role update #1287
## [3.5.0] - 2025-07-31
@@ -143,6 +34,7 @@ and this project adheres to
- ✨(frontend) add duplicate action to doc tree #1175
- ✨(frontend) Interlinking doc #904
- ✨(frontend) add multi columns support for editor #1219
- ✨(api) add API route to fetch document content #1206
### Changed
@@ -803,11 +695,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.1...main
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.5.0...main
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
[v3.4.2]: https://github.com/suitenumerique/docs/releases/v3.4.2
[v3.4.1]: https://github.com/suitenumerique/docs/releases/v3.4.1

View File

@@ -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/

View File

@@ -93,77 +93,13 @@ post-bootstrap: \
mails-build
.PHONY: post-bootstrap
pre-beautiful-bootstrap: ## Display a welcome message before bootstrap
ifeq ($(OS),Windows_NT)
@echo ""
@echo "================================================================================"
@echo ""
@echo " Welcome to Docs - Collaborative Text Editing from La Suite!"
@echo ""
@echo " This will set up your development environment with:"
@echo " - Docker containers for all services"
@echo " - Database migrations and static files"
@echo " - Frontend dependencies and build"
@echo " - Environment configuration files"
@echo ""
@echo " Services will be available at:"
@echo " - Frontend: http://localhost:3000"
@echo " - API: http://localhost:8071"
@echo " - Admin: http://localhost:8071/admin"
@echo ""
@echo "================================================================================"
@echo ""
@echo "Starting bootstrap process..."
else
@echo "$(BOLD)"
@echo "╔══════════════════════════════════════════════════════════════════════════════╗"
@echo "║ ║"
@echo "║ 🚀 Welcome to Docs - Collaborative Text Editing from La Suite ! 🚀 ║"
@echo "║ ║"
@echo "║ This will set up your development environment with : ║"
@echo "║ • Docker containers for all services ║"
@echo "║ • Database migrations and static files ║"
@echo "║ • Frontend dependencies and build ║"
@echo "║ • Environment configuration files ║"
@echo "║ ║"
@echo "║ Services will be available at: ║"
@echo "║ • Frontend: http://localhost:3000 ║"
@echo "║ • API: http://localhost:8071 ║"
@echo "║ • Admin: http://localhost:8071/admin ║"
@echo "║ ║"
@echo "╚══════════════════════════════════════════════════════════════════════════════╝"
@echo "$(RESET)"
@echo "$(GREEN)Starting bootstrap process...$(RESET)"
endif
@echo ""
.PHONY: pre-beautiful-bootstrap
post-beautiful-bootstrap: ## Display a success message after bootstrap
@echo ""
ifeq ($(OS),Windows_NT)
@echo "Bootstrap completed successfully!"
@echo ""
@echo "Next steps:"
@echo " - Visit http://localhost:3000 to access the application"
@echo " - Run 'make help' to see all available commands"
else
@echo "$(GREEN)🎉 Bootstrap completed successfully!$(RESET)"
@echo ""
@echo "$(BOLD)Next steps:$(RESET)"
@echo " • Visit http://localhost:3000 to access the application"
@echo " • Run 'make help' to see all available commands"
endif
@echo ""
.PHONY: post-beautiful-bootstrap
bootstrap: ## Prepare the project for local development
bootstrap: ## Prepare Docker developmentimages for the project
bootstrap: \
pre-beautiful-bootstrap \
pre-bootstrap \
build \
post-bootstrap \
run \
post-beautiful-bootstrap
run
.PHONY: bootstrap
bootstrap-e2e: ## Prepare Docker production images to be used for e2e tests
@@ -406,10 +342,6 @@ run-frontend-development: ## Run the frontend in development mode
cd $(PATH_FRONT_IMPRESS) && yarn dev
.PHONY: run-frontend-development
frontend-test: ## Run the frontend tests
cd $(PATH_FRONT_IMPRESS) && yarn test
.PHONY: frontend-test
frontend-i18n-extract: ## Extract the frontend translation inside a json to be used for crowdin
cd $(PATH_FRONT) && yarn i18n:extract
.PHONY: frontend-i18n-extract
@@ -440,6 +372,6 @@ bump-packages-version: ## bump the version of the project - VERSION_TYPE can be
cd ./src/frontend/apps/e2e/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/apps/impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/servers/y-provider/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/eslint-plugin-docs/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/eslint-config-impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
cd ./src/frontend/packages/i18n/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
.PHONY: bump-packages-version

2
Procfile Normal file
View File

@@ -0,0 +1,2 @@
web: bin/buildpack_start.sh
postdeploy: python manage.py migrate

View File

@@ -49,24 +49,13 @@ Docs is a collaborative text editor designed to address common challenges in kno
* 📚 Turn your team's collaborative work into organized knowledge with Subpages.
### Self-host
🚀 Docs is easy to install on your own servers
#### 🚀 Docs is easy to install on your own servers
We use Kubernetes for our [production instance](https://docs.numerique.gouv.fr/) but also support Docker Compose. The community contributed a couple other methods (Nix, YunoHost etc.) check out the [docs](/docs/installation/README.md) to get detailed instructions and examples.
Available methods: Helm chart, Nix package
#### 🌍 Known instances
We hope to see many more, here is an incomplete list of public Docs instances. Feel free to make a PR to add ones that are not listed below🙏
In the works: Docker Compose, YunoHost
| Url | Org | Public |
| --- | --- | ------- |
| [docs.numerique.gouv.fr](https://docs.numerique.gouv.fr/) | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up|
| [docs.suite.anct.gouv.fr](https://docs.suite.anct.gouv.fr/) | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up|
| [notes.demo.opendesk.eu](https://notes.demo.opendesk.eu) | ZenDiS | Demo instance of OpenDesk. Request access to get credentials |
| [notes.liiib.re](https://notes.liiib.re/) | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
| [docs.federated.nexus](https://docs.federated.nexus/) | federated.nexus | Public instance, but you have to [sign up for a Federated Nexus account](https://federated.nexus/register/). |
| [docs.demo.mosacloud.eu](https://docs.demo.mosacloud.eu/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
#### ⚠️ Advanced features
For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under GPL and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.
## Getting started 🔧
@@ -141,12 +130,6 @@ To start all the services, except the frontend container, you can use the follow
$ make run-backend
```
To execute frontend tests & linting only
```shellscript
$ make frontend-test
$ make frontend-lint
```
**Adding content**
You can create a basic demo site by running this command:

View File

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

15
bin/buildpack_postcompile.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-compile script"
rm -rf docker docs env.d gitlint src/frontend/apps/e2e
rm -rf src/frontend/apps
rm -rf src/frontend/packages
# Remove some of the larger packages required by the frontend only
rm -rf src/frontend/node_modules/@next src/frontend/node_modules/next src/frontend/node_modules/react-icons src/frontend/node_modules/@gouvfr-lasuite
# du -ch | sort -rh | head -n 100

15
bin/buildpack_postfrontend.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-frontend script"
# Move the frontend build to the nginx root and clean up
mkdir -p build/
mv src/frontend/apps/impress/out build/frontend-out
mv src/backend/* ./
mv src/nginx/* ./
echo "3.13" > .python-version

18
bin/buildpack_start.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Start the Django backend server
gunicorn -b :8000 impress.wsgi:application --log-file - &
# Start the Y provider service
cd src/frontend/servers/y-provider && PORT=4444 ../../.scalingo/node/bin/node dist/start-server.js &
# Start the Nginx server
bin/run &
# if the current shell is killed, also terminate all its children
trap "pkill SIGTERM -P $$" SIGTERM
# wait for a single child to finish,
wait -n
# then kill all the other tasks
pkill -P $$

View File

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

View File

@@ -26,7 +26,7 @@
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"enabled": true,
"sslRequired": "none",
"sslRequired": "external",
"registrationAllowed": true,
"registrationEmailAsUsername": false,
"rememberMe": true,
@@ -60,7 +60,7 @@
},
{
"username": "user-e2e-chromium",
"email": "user.test@chromium.test",
"email": "user@chromium.test",
"firstName": "E2E",
"lastName": "Chromium",
"enabled": true,
@@ -74,7 +74,7 @@
},
{
"username": "user-e2e-webkit",
"email": "user.test@webkit.test",
"email": "user@webkit.test",
"firstName": "E2E",
"lastName": "Webkit",
"enabled": true,
@@ -88,7 +88,7 @@
},
{
"username": "user-e2e-firefox",
"email": "user.test@firefox.test",
"email": "user@firefox.test",
"firstName": "E2E",
"lastName": "Firefox",
"enabled": true,
@@ -2270,7 +2270,7 @@
"cibaInterval": "5",
"realmReusableOtpCode": "false"
},
"keycloakVersion": "26.3.2",
"keycloakVersion": "20.0.1",
"userManagedAccessAllowed": false,
"clientProfiles": {
"profiles": []

View File

@@ -11,9 +11,6 @@ server {
server_name localhost;
charset utf-8;
# increase max upload size
client_max_body_size 10m;
# Disables server version feedback on pages and in headers
server_tokens off;
@@ -71,7 +68,7 @@ server {
proxy_set_header Host $host;
}
location /collaboration/api/ {
location /collaboration/api/ {
# Collaboration server
proxy_pass http://${YPROVIDER_HOST}:4444;
proxy_set_header Host $host;
@@ -98,7 +95,7 @@ server {
add_header Content-Security-Policy "default-src 'none'" always;
}
location /media-auth {
proxy_pass http://docs_backend/api/v1.0/documents/media-auth/;
proxy_set_header X-Forwarded-Proto https;
@@ -112,4 +109,4 @@ server {
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
}
}
}

View File

@@ -135,9 +135,9 @@ NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
| PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true |
Packages with licences incompatible with the MIT licence:
* `xl-docx-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
* `xl-multi-column`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE).
* `xl-docx-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
* `xl-multi-column`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE).
In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features.

View File

@@ -7,12 +7,12 @@ services:
timeout: 2s
retries: 300
env_file:
- env.d/postgresql
- env.d/common
- env.d/postgresql
- env.d/common
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ./data/databases/backend:/var/lib/postgresql/data/pgdata
- ./data/databases/backend:/var/lib/postgresql/data/pgdata
redis:
image: redis:8
@@ -22,12 +22,12 @@ services:
user: ${DOCKER_USER:-1000}
restart: always
environment:
- DJANGO_CONFIGURATION=Production
- DJANGO_CONFIGURATION=Production
env_file:
- env.d/common
- env.d/backend
- env.d/yprovider
- env.d/postgresql
- env.d/common
- env.d/backend
- env.d/yprovider
- env.d/postgresql
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 15s
@@ -45,24 +45,24 @@ services:
image: lasuite/impress-y-provider:latest
user: ${DOCKER_USER:-1000}
env_file:
- env.d/common
- env.d/yprovider
- env.d/common
- env.d/yprovider
frontend:
image: lasuite/impress-frontend:latest
user: "101"
entrypoint:
- /docker-entrypoint.sh
- /docker-entrypoint.sh
command: ["nginx", "-g", "daemon off;"]
env_file:
- env.d/common
- env.d/common
# Uncomment and set your values if using our nginx proxy example
#environment:
# - VIRTUAL_HOST=${DOCS_HOST} # used by nginx proxy
# - VIRTUAL_HOST=${DOCS_HOST} # used by nginx proxy
# - VIRTUAL_PORT=8083 # used by nginx proxy
# - LETSENCRYPT_HOST=${DOCS_HOST} # used by lets encrypt to generate TLS certificate
volumes:
- ./default.conf.template:/etc/nginx/templates/docs.conf.template
- ./default.conf.template:/etc/nginx/templates/docs.conf.template
depends_on:
backend:
condition: service_healthy

View File

@@ -7,23 +7,23 @@ services:
timeout: 2s
retries: 300
env_file:
- env.d/kc_postgresql
- env.d/kc_postgresql
volumes:
- ./data/keycloak:/var/lib/postgresql/data/pgdata
- ./data/keycloak:/var/lib/postgresql/data/pgdata
keycloak:
image: quay.io/keycloak/keycloak:26.1.3
command: ["start"]
env_file:
- env.d/kc_postgresql
- env.d/keycloak
- env.d/kc_postgresql
- env.d/keycloak
# Uncomment and set your values if using our nginx proxy example
# environment:
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
# - VIRTUAL_PORT=8080 # used by nginx proxy
# - LETSENCRYPT_HOST=id.yourdomain.tld # used by lets encrypt to generate TLS certificate
depends_on:
kc_postgresql:
kc_postgresql::
condition: service_healthy
restart: true
# Uncomment if using our nginx proxy example
@@ -33,4 +33,4 @@ services:
#
#networks:
# proxy-tier:
# external: true
# external: true

View File

@@ -2,8 +2,8 @@ services:
minio:
image: minio/minio
environment:
- MINIO_ROOT_USER=<set minio root username>
- MINIO_ROOT_PASSWORD=<set minio root password>
- MINIO_ROOT_USER=<set minio root username>
- MINIO_ROOT_PASSWORD=<set minio root password>
# Uncomment and set your values if using our nginx proxy example
# - VIRTUAL_HOST=storage.yourdomain.tld # used by nginx proxy
# - VIRTUAL_PORT=9000 # used by nginx proxy
@@ -16,12 +16,12 @@ services:
entrypoint: ""
command: minio server /data
volumes:
- ./data/minio:/data
- ./data/minio:/data
# Uncomment if using our nginx proxy example
# networks:
# - proxy-tier
# - proxy-tier
# Uncomment if using our nginx proxy example
#networks:
# proxy-tier:
# external: true
# external: true

View File

@@ -3,28 +3,28 @@ services:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
- "80:80"
- "443:443"
volumes:
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- proxy-tier
- proxy-tier
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
environment:
- DEFAULT_EMAIL=mail@yourdomain.tld
- DEFAULT_EMAIL=mail@yourdomain.tld
volumes_from:
- nginx-proxy
- nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy-tier
- proxy-tier
networks:
proxy-tier:

View File

@@ -27,7 +27,7 @@ backend:
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/session/end
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
@@ -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

View File

@@ -1,32 +0,0 @@
# Installation
If you want to install Docs you've come to the right place.
Here are a bunch of resources to help you install the project.
## Kubernetes
We (Docs maintainers) are only using the Kubernetes deployment method in production. We can only provide advanced support for this method.
Please follow the instructions laid out [here](/docs/installation/kubernetes.md).
## Docker Compose
We are aware that not everyone has Kubernetes Cluster laying around 😆.
We also provide [Docker images](https://hub.docker.com/u/lasuite?page=1&search=impress) that you can deploy using Compose.
Please follow the instructions [here](/docs/installation/compose.md).
⚠️ Please keep in mind that we do not use it ourselves in production. Let us know in the issues if you run into troubles, we'll try to help.
## Other ways to install Docs
Community members have contributed several other ways to install Docs. While we owe them a big thanks 🙏, please keep in mind we (Docs maintainers) can't provide support on these installation methods as we don't use them ourselves and there are two many options out there for us to keep track of. Of course you can contact the contributors and the broader community for assistance.
Here is the list of other methods in alphabetical order:
- Coop-Cloud: [code](https://git.coopcloud.tech/coop-cloud/lasuite-docs)
- Nix: [Packages](https://search.nixos.org/packages?channel=unstable&query=lasuite-docs), ⚠️ unstable
- Podman: [code][https://codeberg.org/philo/lasuite-docs-podman], ⚠️ experimental
- YunoHost: [code](https://github.com/YunoHost-Apps/lasuite-docs_ynh), [app store](https://apps.yunohost.org/app/lasuite-docs)
Feel free to make a PR to add ones that are not listed above 🙏
## Cloud providers
Some cloud providers are making it easy to deploy Docs on their infrastructure.
Here is the list in alphabetical order:
- Clever Cloud 🇫🇷 : [market place][https://www.clever-cloud.com/product/docs/], [technical doc](https://www.clever.cloud/developers/guides/docs/#deploy-docs)
Feel free to make a PR to add ones that are not listed above 🙏

View File

@@ -124,7 +124,7 @@ OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/session/end
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256

View File

@@ -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.

View File

@@ -66,6 +66,3 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
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

View File

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

View File

@@ -43,8 +43,8 @@ OIDC_RP_CLIENT_ID=<client_id>
OIDC_RP_CLIENT_SECRET=<client secret>
OIDC_RP_SIGN_ALGO=RS256
OIDC_RP_SCOPES="openid email"
#OIDC_USERINFO_SHORTNAME_FIELD
#OIDC_USERINFO_FULLNAME_FIELDS
#USER_OIDC_FIELD_TO_SHORTNAME
#USER_OIDC_FIELDS_TO_FULLNAME
LOGIN_REDIRECT_URL=https://${DOCS_HOST}
LOGIN_REDIRECT_URL_FAILURE=https://${DOCS_HOST}

View File

@@ -2,10 +2,6 @@
"extends": ["github>numerique-gouv/renovate-configuration"],
"dependencyDashboard": true,
"labels": ["dependencies", "noChangeLog", "automated"],
"schedule": ["before 7am on monday"],
"prCreation": "not-pending",
"rebaseWhen": "conflicted",
"updateNotScheduled": false,
"packageRules": [
{
"enabled": false,
@@ -27,6 +23,7 @@
"@hocuspocus/provider",
"@hocuspocus/server",
"docx",
"eslint",
"fetch-mock",
"node",
"node-fetch",

View File

@@ -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",

View File

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

View File

@@ -7,13 +7,12 @@ from base64 import b64decode
from django.conf import settings
from django.db.models import Q
from django.utils.functional import lazy
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
import magic
from rest_framework import serializers
from core import choices, enums, models, utils, validators
from core import choices, enums, models, utils
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
ConversionError,
@@ -33,30 +32,11 @@ class UserSerializer(serializers.ModelSerializer):
class UserLightSerializer(UserSerializer):
"""Serialize users with limited fields."""
full_name = serializers.SerializerMethodField(read_only=True)
short_name = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.User
fields = ["full_name", "short_name"]
read_only_fields = ["full_name", "short_name"]
def get_full_name(self, instance):
"""Return the full name of the user."""
if not instance.full_name:
email = instance.email.split("@")[0]
return slugify(email)
return instance.full_name
def get_short_name(self, instance):
"""Return the short name of the user."""
if not instance.short_name:
email = instance.email.split("@")[0]
return slugify(email)
return instance.short_name
class TemplateAccessSerializer(serializers.ModelSerializer):
"""Serialize template accesses."""
@@ -90,7 +70,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 +82,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -126,7 +104,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -168,10 +145,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 +173,6 @@ class DocumentSerializer(ListDocumentSerializer):
"content",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -224,7 +196,6 @@ class DocumentSerializer(ListDocumentSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"is_favorite",
"link_role",
@@ -431,7 +402,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
content = serializers.CharField(required=True)
# User
sub = serializers.CharField(
required=True, validators=[validators.sub_validator], max_length=255
required=True, validators=[models.User.sub_validator], max_length=255
)
email = serializers.EmailField(required=True)
language = serializers.ChoiceField(
@@ -515,10 +486,6 @@ class LinkDocumentSerializer(serializers.ModelSerializer):
We expose it separately from document in order to simplify and secure access control.
"""
link_reach = serializers.ChoiceField(
choices=models.LinkReachChoices.choices, required=True
)
class Meta:
model = models.Document
fields = [
@@ -526,58 +493,6 @@ class LinkDocumentSerializer(serializers.ModelSerializer):
"link_reach",
]
def validate(self, attrs):
"""Validate that link_role and link_reach are compatible using get_select_options."""
link_reach = attrs.get("link_reach")
link_role = attrs.get("link_role")
if not link_reach:
raise serializers.ValidationError(
{"link_reach": _("This field is required.")}
)
# Get available options based on ancestors' link definition
available_options = models.LinkReachChoices.get_select_options(
**self.instance.ancestors_link_definition
)
# Validate link_reach is allowed
if link_reach not in available_options:
msg = _(
"Link reach '%(link_reach)s' is not allowed based on parent document configuration."
)
raise serializers.ValidationError(
{"link_reach": msg % {"link_reach": link_reach}}
)
# Validate link_role is compatible with link_reach
allowed_roles = available_options[link_reach]
# Restricted reach: link_role must be None
if link_reach == models.LinkReachChoices.RESTRICTED:
if link_role is not None:
raise serializers.ValidationError(
{
"link_role": (
"Cannot set link_role when link_reach is 'restricted'. "
"Link role must be null for restricted reach."
)
}
)
return attrs
# Non-restricted: link_role must be in allowed roles
if link_role not in allowed_roles:
allowed_roles_str = ", ".join(allowed_roles) if allowed_roles else "none"
raise serializers.ValidationError(
{
"link_role": (
f"Link role '{link_role}' is not allowed for link reach '{link_reach}'. "
f"Allowed roles: {allowed_roles_str}"
)
}
)
return attrs
class DocumentDuplicationSerializer(serializers.Serializer):
"""
@@ -749,9 +664,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):

View File

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

View File

@@ -14,7 +14,6 @@ from django.contrib.postgres.search import TrigramSimilarity
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.core.validators import URLValidator
from django.db import connection, transaction
from django.db import models as db
from django.db.models.expressions import RawSQL
@@ -34,6 +33,7 @@ from lasuite.malware_detection import malware_detection
from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
from rest_framework.permissions import AllowAny
from rest_framework.throttling import UserRateThrottle
from core import authentication, choices, enums, models
from core.services.ai_services import AIService
@@ -51,8 +51,7 @@ from core.tasks.mail import send_ask_for_access_mail
from core.utils import extract_attachments, filter_descendants
from . import permissions, serializers, utils
from .filters import DocumentFilter, ListDocumentFilter, UserSearchFilter
from .throttling import UserListThrottleBurst, UserListThrottleSustained
from .filters import DocumentFilter, ListDocumentFilter
logger = logging.getLogger(__name__)
@@ -146,6 +145,18 @@ class Pagination(drf.pagination.PageNumberPagination):
page_size_query_param = "page_size"
class UserListThrottleBurst(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_burst"
class UserListThrottleSustained(UserRateThrottle):
"""Throttle for the user list endpoint."""
scope = "user_list_sustained"
class UserViewSet(
drf.mixins.UpdateModelMixin, viewsets.GenericViewSet, drf.mixins.ListModelMixin
):
@@ -176,18 +187,12 @@ class UserViewSet(
if self.action != "list":
return queryset
filterset = UserSearchFilter(
self.request.GET, queryset=queryset, request=self.request
)
if not filterset.is_valid():
raise drf.exceptions.ValidationError(filterset.errors)
# Exclude all users already in the given document
if document_id := self.request.query_params.get("document_id", ""):
queryset = queryset.exclude(documentaccess__document_id=document_id)
filter_data = filterset.form.cleaned_data
query = filter_data["q"]
if not (query := self.request.query_params.get("q", "")) or len(query) < 5:
return queryset.none()
# For emails, match emails by Levenstein distance to prevent typing errors
if "@" in query:
@@ -364,8 +369,7 @@ class DocumentViewSet(
permission_classes = [
permissions.DocumentPermission,
]
throttle_scope = "document"
queryset = models.Document.objects.select_related("creator").all()
queryset = models.Document.objects.all()
serializer_class = serializers.DocumentSerializer
ai_translate_serializer_class = serializers.AITranslateSerializer
children_serializer_class = serializers.ListDocumentSerializer
@@ -623,29 +627,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)
@@ -809,11 +796,7 @@ class DocumentViewSet(
)
# GET: List children
queryset = (
document.get_children()
.select_related("creator")
.filter(ancestors_deleted_at__isnull=True)
)
queryset = document.get_children().filter(ancestors_deleted_at__isnull=True)
queryset = self.filter_queryset(queryset)
filterset = DocumentFilter(request.GET, queryset=queryset)
@@ -867,48 +850,26 @@ class DocumentViewSet(
user = self.request.user
try:
current_document = (
self.queryset.select_related(None)
.only("depth", "path", "ancestors_deleted_at")
.get(pk=pk)
)
current_document = self.queryset.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
ancestors = (
(current_document.get_ancestors() | self.queryset.filter(pk=pk))
.filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
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.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()
)
# 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()
@@ -929,12 +890,7 @@ class DocumentViewSet(
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
queryset = (
ancestors.select_related("creator").filter(
depth__gte=highest_readable.depth
)
| children
)
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
queryset = queryset.order_by("path")
queryset = queryset.annotate_user_roles(user)
queryset = queryset.annotate_is_favorite(user)
@@ -972,64 +928,37 @@ class DocumentViewSet(
in the payload.
"""
# Get document while checking permissions
document_to_duplicate = self.get_object()
document = self.get_object()
serializer = serializers.DocumentDuplicationSerializer(
data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
with_accesses = serializer.validated_data.get("with_accesses", False)
user_role = document_to_duplicate.get_role(request.user)
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
is_owner_or_admin = document.get_role(request.user) in models.PRIVILEGED_ROLES
base64_yjs_content = document_to_duplicate.content
base64_yjs_content = document.content
# Duplicate the document instance
link_kwargs = (
{
"link_reach": document_to_duplicate.link_reach,
"link_role": document_to_duplicate.link_role,
}
{"link_reach": document.link_reach, "link_role": document.link_role}
if with_accesses
else {}
)
extracted_attachments = set(extract_attachments(document_to_duplicate.content))
attachments = list(
extracted_attachments & set(document_to_duplicate.attachments)
)
title = capfirst(_("copy of {title}").format(title=document_to_duplicate.title))
if not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
user_role
) < choices.RoleChoices.get_priority(models.RoleChoices.EDITOR):
duplicated_document = models.Document.add_root(
creator=self.request.user,
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document_to_duplicate,
**link_kwargs,
)
models.DocumentAccess.objects.create(
document=duplicated_document,
user=self.request.user,
role=models.RoleChoices.OWNER,
)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
)
duplicated_document = document_to_duplicate.add_sibling(
extracted_attachments = set(extract_attachments(document.content))
attachments = list(extracted_attachments & set(document.attachments))
duplicated_document = document.add_sibling(
"right",
title=title,
title=capfirst(_("copy of {title}").format(title=document.title)),
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document_to_duplicate,
duplicated_from=document,
creator=request.user,
**link_kwargs,
)
# Always add the logged-in user as OWNER for root documents
if document_to_duplicate.is_root():
if document.is_root():
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
@@ -1041,7 +970,7 @@ class DocumentViewSet(
# If accesses should be duplicated, add other users' accesses as per original document
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document_to_duplicate
document=document
).exclude(user=request.user)
accesses_to_create.extend(
@@ -1363,8 +1292,7 @@ class DocumentViewSet(
)
attachments_documents = (
self.queryset.select_related(None)
.filter(attachments__contains=[key])
self.queryset.filter(attachments__contains=[key])
.only("path")
.order_by("path")
)
@@ -1523,15 +1451,6 @@ class DocumentViewSet(
url = unquote(url)
url_validator = URLValidator(schemes=["http", "https"])
try:
url_validator(url)
except drf.exceptions.ValidationError as e:
return drf.response.Response(
{"detail": str(e)},
status=drf.status.HTTP_400_BAD_REQUEST,
)
try:
response = requests.get(
url,
@@ -1562,10 +1481,10 @@ class DocumentViewSet(
return proxy_response
except requests.RequestException as e:
logger.exception(e)
return drf.response.Response(
{"error": f"Failed to fetch resource from {url}"},
status=status.HTTP_400_BAD_REQUEST,
logger.error("Proxy request failed: %s", str(e))
return drf_response.Response(
{"error": f"Failed to fetch resource: {e!s}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@drf.decorators.action(
@@ -1681,7 +1600,6 @@ class DocumentAccessViewSet(
"document__depth",
)
resource_field_name = "document"
throttle_scope = "document_access"
@cached_property
def document(self):
@@ -1841,7 +1759,6 @@ class TemplateViewSet(
permissions.IsAuthenticatedOrSafe,
permissions.ResourceWithAccessPermission,
]
throttle_scope = "template"
ordering = ["-created_at"]
ordering_fields = ["created_at", "updated_at", "title"]
serializer_class = serializers.TemplateSerializer
@@ -1932,7 +1849,6 @@ class TemplateAccessViewSet(
lookup_field = "pk"
permission_classes = [permissions.ResourceAccessPermission]
throttle_scope = "template_access"
queryset = models.TemplateAccess.objects.select_related("user").all()
resource_field_name = "template"
serializer_class = serializers.TemplateAccessSerializer
@@ -2015,7 +1931,6 @@ class InvitationViewset(
permissions.CanCreateInvitationPermission,
permissions.ResourceWithAccessPermission,
]
throttle_scope = "invitation"
queryset = (
models.Invitation.objects.all()
.select_related("document")
@@ -2094,7 +2009,6 @@ class DocumentAskForAccessViewSet(
permissions.IsAuthenticated,
permissions.ResourceWithAccessPermission,
]
throttle_scope = "document_ask_for_access"
queryset = models.DocumentAskForAccess.objects.all()
serializer_class = serializers.DocumentAskForAccessSerializer
_document = None
@@ -2167,7 +2081,6 @@ class ConfigView(drf.views.APIView):
"""API ViewSet for sharing some public settings."""
permission_classes = [AllowAny]
throttle_scope = "config"
def get(self, request):
"""
@@ -2188,7 +2101,6 @@ class ConfigView(drf.views.APIView):
"LANGUAGES",
"LANGUAGE_CODE",
"SENTRY_DSN",
"TRASHBIN_CUTOFF_DAYS",
]
dict_settings = {}
for setting in array_settings:

View File

@@ -2,8 +2,6 @@
from django.db import migrations, models
import core.validators
class Migration(migrations.Migration):
dependencies = [
@@ -35,17 +33,4 @@ class Migration(migrations.Migration):
verbose_name="language",
),
),
migrations.AlterField(
model_name="user",
name="sub",
field=models.CharField(
blank=True,
help_text="Required. 255 characters or fewer. ASCII characters only.",
max_length=255,
null=True,
unique=True,
validators=[core.validators.sub_validator],
verbose_name="sub",
),
),
]

View File

@@ -14,7 +14,7 @@ from django.contrib.auth import models as auth_models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.postgres.fields import ArrayField
from django.contrib.sites.models import Site
from django.core import mail
from django.core import mail, validators
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
@@ -39,7 +39,6 @@ from .choices import (
RoleChoices,
get_equivalent_link_definition,
)
from .validators import sub_validator
logger = getLogger(__name__)
@@ -137,12 +136,22 @@ class UserManager(auth_models.UserManager):
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
"""User model to work with OIDC only authentication."""
sub_validator = validators.RegexValidator(
regex=r"^[\w.@+-:]+\Z",
message=_(
"Enter a valid sub. This value may contain only letters, "
"numbers, and @/./+/-/_/: characters."
),
)
sub = models.CharField(
_("sub"),
help_text=_("Required. 255 characters or fewer. ASCII characters only."),
help_text=_(
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
),
max_length=255,
validators=[sub_validator],
unique=True,
validators=[sub_validator],
blank=True,
null=True,
)
@@ -221,7 +230,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)
@@ -721,7 +730,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,16 +759,9 @@ 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
can_create_children = can_update and user.is_authenticated
can_destroy = (
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(
@@ -782,24 +784,24 @@ class Document(MP_Node, BaseModel):
"media_check": can_get,
"can_edit": can_update,
"children_list": can_get,
"children_create": can_create_children,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,
"content": can_get,
"cors_proxy": can_get,
"descendants": can_get,
"destroy": can_destroy,
"destroy": is_owner,
"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,

View File

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

View File

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

View File

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

View File

@@ -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),

View File

@@ -2,7 +2,6 @@
import pytest
import responses
from requests.exceptions import RequestException
from rest_framework.test import APIClient
from core import factories
@@ -150,41 +149,3 @@ def test_api_docs_cors_proxy_unsupported_media_type():
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 415
@pytest.mark.parametrize(
"url_to_fetch",
[
"ftp://external-url.com/assets/index.html",
"ftps://external-url.com/assets/index.html",
"invalid-url.com",
"ssh://external-url.com/assets/index.html",
],
)
def test_api_docs_cors_proxy_invalid_url(url_to_fetch):
"""Test the CORS proxy API for documents with an invalid URL."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 400
assert response.json() == ["Enter a valid URL."]
@responses.activate
def test_api_docs_cors_proxy_request_failed():
"""Test the CORS proxy API for documents with a request failed."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
url_to_fetch = "https://external-url.com/assets/index.html"
responses.get(url_to_fetch, body=RequestException("Connection refused"))
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 400
assert response.json() == {
"error": "Failed to fetch resource from https://external-url.com/assets/index.html"
}

View File

@@ -148,7 +148,7 @@ def test_api_documents_create_for_owner_invalid_sub():
data = {
"title": "My Document",
"content": "Document content",
"sub": "invalid süb",
"sub": "123!!",
"email": "john.doe@example.com",
}
@@ -163,7 +163,10 @@ def test_api_documents_create_for_owner_invalid_sub():
assert not Document.objects.exists()
assert response.json() == {
"sub": ["Enter a valid sub. This value should be ASCII only."]
"sub": [
"Enter a valid sub. This value may contain only letters, "
"numbers, and @/./+/-/_/: characters."
]
}

View File

@@ -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),

View File

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

View File

@@ -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,

View File

@@ -133,10 +133,7 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED,
link_role=models.LinkRoleChoices.READER,
)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
@@ -146,10 +143,7 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
)
new_document_values = serializers.LinkDocumentSerializer(
instance=factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.EDITOR,
)
instance=factories.DocumentFactory()
).data
with mock_reset_connections(document.id):
@@ -164,240 +158,3 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
document_values = serializers.LinkDocumentSerializer(instance=document).data
for key, value in document_values.items():
assert value == new_document_values[key]
def test_api_documents_link_configuration_update_role_restricted_forbidden():
"""
Test that trying to set link_role on a document with restricted link_reach
returns a validation error.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.RESTRICTED,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
# Try to set a meaningful role on a restricted document
new_data = {
"link_reach": models.LinkReachChoices.RESTRICTED,
"link_role": models.LinkRoleChoices.EDITOR,
}
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_role" in response.json()
assert (
"Cannot set link_role when link_reach is 'restricted'"
in response.json()["link_role"][0]
)
def test_api_documents_link_configuration_update_link_reach_required():
"""
Test that link_reach is required when updating link configuration.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
# Try to update without providing link_reach
new_data = {"link_role": models.LinkRoleChoices.EDITOR}
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_reach" in response.json()
assert "This field is required" in response.json()["link_reach"][0]
def test_api_documents_link_configuration_update_restricted_without_role_success(
mock_reset_connections, # pylint: disable=redefined-outer-name
):
"""
Test that setting link_reach to restricted without specifying link_role succeeds.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
# Only specify link_reach, not link_role
new_data = {
"link_reach": models.LinkReachChoices.RESTRICTED,
}
with mock_reset_connections(document.id):
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 200
document.refresh_from_db()
assert document.link_reach == models.LinkReachChoices.RESTRICTED
@pytest.mark.parametrize(
"reach", [models.LinkReachChoices.PUBLIC, models.LinkReachChoices.AUTHENTICATED]
)
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
def test_api_documents_link_configuration_update_non_restricted_with_valid_role_success(
reach,
role,
mock_reset_connections, # pylint: disable=redefined-outer-name
):
"""
Test that setting non-restricted link_reach with valid link_role succeeds.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.RESTRICTED,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
new_data = {
"link_reach": reach,
"link_role": role,
}
with mock_reset_connections(document.id):
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 200
document.refresh_from_db()
assert document.link_reach == reach
assert document.link_role == role
def test_api_documents_link_configuration_update_with_ancestor_constraints():
"""
Test that link configuration respects ancestor constraints using get_select_options.
This test may need adjustment based on the actual get_select_options implementation.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent_document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
child_document = factories.DocumentFactory(
parent=parent_document,
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=child_document, user=user, role=models.RoleChoices.OWNER
)
# Try to set child to PUBLIC when parent is RESTRICTED
new_data = {
"link_reach": models.LinkReachChoices.RESTRICTED,
"link_role": models.LinkRoleChoices.READER,
}
response = client.put(
f"/api/v1.0/documents/{child_document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_reach" in response.json()
assert (
"Link reach 'restricted' is not allowed based on parent"
in response.json()["link_reach"][0]
)
def test_api_documents_link_configuration_update_invalid_role_for_reach_validation():
"""
Test the specific validation logic that checks if link_role is allowed for link_reach.
This tests the code section that validates allowed_roles from get_select_options.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent_document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED,
link_role=models.LinkRoleChoices.EDITOR,
)
child_document = factories.DocumentFactory(
parent=parent_document,
link_reach=models.LinkReachChoices.RESTRICTED,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=child_document, user=user, role=models.RoleChoices.OWNER
)
new_data = {
"link_reach": models.LinkReachChoices.AUTHENTICATED,
"link_role": models.LinkRoleChoices.READER, # This should be rejected
}
response = client.put(
f"/api/v1.0/documents/{child_document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_role" in response.json()
error_message = response.json()["link_role"][0]
assert (
"Link role 'reader' is not allowed for link reach 'authenticated'"
in error_message
)
assert "Allowed roles: editor" in error_message

View File

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

View File

@@ -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,
@@ -504,7 +499,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": access.role in ["administrator", "owner"],
"destroy": access.role == "owner",
"duplicate": True,
"favorite": True,
"invite_owner": access.role == "owner",
@@ -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,

View File

@@ -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

View File

@@ -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")

View File

@@ -1,44 +0,0 @@
"""Test user light serializer."""
import pytest
from core import factories
from core.api.serializers import UserLightSerializer
pytestmark = pytest.mark.django_db
def test_user_light_serializer():
"""Test user light serializer."""
user = factories.UserFactory(
email="test@test.com",
full_name="John Doe",
short_name="John",
)
serializer = UserLightSerializer(user)
assert serializer.data["full_name"] == "John Doe"
assert serializer.data["short_name"] == "John"
def test_user_light_serializer_no_full_name():
"""Test user light serializer without full name."""
user = factories.UserFactory(
email="test_foo@test.com",
full_name=None,
short_name="John",
)
serializer = UserLightSerializer(user)
assert serializer.data["full_name"] == "test_foo"
assert serializer.data["short_name"] == "John"
def test_user_light_serializer_no_short_name():
"""Test user light serializer without short name."""
user = factories.UserFactory(
email="test_foo@test.com",
full_name=None,
short_name=None,
)
serializer = UserLightSerializer(user)
assert serializer.data["full_name"] == "test_foo"
assert serializer.data["short_name"] == "test_foo"

View File

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

View File

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

View File

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

View File

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

View File

@@ -375,42 +375,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(
@@ -635,86 +601,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
}
@pytest.mark.parametrize(
"is_authenticated, is_creator,role,link_reach,link_role,can_destroy",
[
(True, False, "owner", "restricted", "editor", True),
(True, True, "owner", "restricted", "editor", True),
(True, False, "owner", "restricted", "reader", True),
(True, True, "owner", "restricted", "reader", True),
(True, False, "owner", "authenticated", "editor", True),
(True, True, "owner", "authenticated", "editor", True),
(True, False, "owner", "authenticated", "reader", True),
(True, True, "owner", "authenticated", "reader", True),
(True, False, "owner", "public", "editor", True),
(True, True, "owner", "public", "editor", True),
(True, False, "owner", "public", "reader", True),
(True, True, "owner", "public", "reader", True),
(True, False, "administrator", "restricted", "editor", True),
(True, True, "administrator", "restricted", "editor", True),
(True, False, "administrator", "restricted", "reader", True),
(True, True, "administrator", "restricted", "reader", True),
(True, False, "administrator", "authenticated", "editor", True),
(True, True, "administrator", "authenticated", "editor", True),
(True, False, "administrator", "authenticated", "reader", True),
(True, True, "administrator", "authenticated", "reader", True),
(True, False, "administrator", "public", "editor", True),
(True, True, "administrator", "public", "editor", True),
(True, False, "administrator", "public", "reader", True),
(True, True, "administrator", "public", "reader", True),
(True, False, "editor", "restricted", "editor", False),
(True, True, "editor", "restricted", "editor", True),
(True, False, "editor", "restricted", "reader", False),
(True, True, "editor", "restricted", "reader", True),
(True, False, "editor", "authenticated", "editor", False),
(True, True, "editor", "authenticated", "editor", True),
(True, False, "editor", "authenticated", "reader", False),
(True, True, "editor", "authenticated", "reader", True),
(True, False, "editor", "public", "editor", False),
(True, True, "editor", "public", "editor", True),
(True, False, "editor", "public", "reader", False),
(True, True, "editor", "public", "reader", True),
(True, False, "reader", "restricted", "editor", False),
(True, False, "reader", "restricted", "reader", False),
(True, False, "reader", "authenticated", "editor", False),
(True, True, "reader", "authenticated", "editor", True),
(True, False, "reader", "authenticated", "reader", False),
(True, False, "reader", "public", "editor", False),
(True, True, "reader", "public", "editor", True),
(True, False, "reader", "public", "reader", False),
(False, False, None, "restricted", "editor", False),
(False, False, None, "restricted", "reader", False),
(False, False, None, "authenticated", "editor", False),
(False, False, None, "authenticated", "reader", False),
(False, False, None, "public", "editor", False),
(False, False, None, "public", "reader", False),
],
)
# pylint: disable=too-many-arguments, too-many-positional-arguments
def test_models_documents_get_abilities_children_destroy( # noqa: PLR0913
is_authenticated,
is_creator,
role,
link_reach,
link_role,
can_destroy,
):
"""For a sub document, if a user can create children, he can destroy it."""
user = factories.UserFactory() if is_authenticated else AnonymousUser()
parent = factories.DocumentFactory(link_reach=link_reach, link_role=link_role)
document = factories.DocumentFactory(
link_reach=link_reach,
link_role=link_role,
parent=parent,
creator=user if is_creator else None,
)
if is_authenticated:
factories.UserDocumentAccessFactory(document=parent, user=user, role=role)
abilities = document.get_abilities(user)
assert abilities["destroy"] is can_destroy
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.parametrize(
"is_authenticated,reach",

View File

@@ -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
@@ -44,55 +44,3 @@ def test_models_users_send_mail_main_missing():
user.email_user("my subject", "my message")
assert str(excinfo.value) == "User has no email address."
@pytest.mark.parametrize(
"sub,is_valid",
[
("valid_sub.@+-:=/", True),
("invalid süb", False),
(12345, True),
],
)
def test_models_users_sub_validator(sub, is_valid):
"""The "sub" field should be validated."""
user = factories.UserFactory()
user.sub = sub
if is_valid:
user.full_clean()
else:
with pytest.raises(
ValidationError,
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()

View File

@@ -1,9 +0,0 @@
"""Custom validators for the core app."""
from django.core.exceptions import ValidationError
def sub_validator(value):
"""Validate that the sub is ASCII only."""
if not value.isascii():
raise ValidationError("Enter a valid sub. This value should be ASCII only.")

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@
},
"externalLinks": [
{
"label": "GitHub",
"label": "Github",
"href": "https://github.com/suitenumerique/docs/"
},
{

View File

@@ -16,6 +16,7 @@ from socket import gethostbyname, gethostname
from django.utils.translation import gettext_lazy as _
import dj_database_url
import sentry_sdk
from configurations import Configuration, values
from csp.constants import NONE
@@ -78,7 +79,9 @@ class Base(Configuration):
# Database
DATABASES = {
"default": {
"default": dj_database_url.config()
if os.environ.get("DATABASE_URL")
else {
"ENGINE": values.Value(
"django.db.backends.postgresql",
environ_name="DB_ENGINE",
@@ -142,7 +145,7 @@ class Base(Configuration):
)
# Document images
DOCUMENT_IMAGE_MAX_SIZE = values.IntegerValue(
DOCUMENT_IMAGE_MAX_SIZE = values.Value(
10 * (2**20), # 10MB
environ_name="DOCUMENT_IMAGE_MAX_SIZE",
environ_prefix=None,
@@ -356,9 +359,6 @@ class Base(Configuration):
"PAGE_SIZE": 20,
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_THROTTLE_CLASSES": [
"lasuite.drf.throttling.MonitoredScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"user_list_sustained": values.Value(
default="180/hour",
@@ -370,46 +370,8 @@ class Base(Configuration):
environ_name="API_USERS_LIST_THROTTLE_RATE_BURST",
environ_prefix=None,
),
"document": values.Value(
default="80/minute",
environ_name="API_DOCUMENT_THROTTLE_RATE",
environ_prefix=None,
),
"document_access": values.Value(
default="50/minute",
environ_name="API_DOCUMENT_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"template": values.Value(
default="30/minute",
environ_name="API_TEMPLATE_THROTTLE_RATE",
environ_prefix=None,
),
"template_access": values.Value(
default="30/minute",
environ_name="API_TEMPLATE_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"invitation": values.Value(
default="60/minute",
environ_name="API_INVITATION_THROTTLE_RATE",
environ_prefix=None,
),
"document_ask_for_access": values.Value(
default="30/minute",
environ_name="API_DOCUMENT_ASK_FOR_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"config": values.Value(
default="30/minute",
environ_name="API_CONFIG_THROTTLE_RATE",
environ_prefix=None,
),
},
}
MONITORED_THROTTLE_FAILURE_CALLBACK = (
"core.api.throttling.sentry_monitoring_throttle_failure"
)
SPECTACULAR_SETTINGS = {
"TITLE": "Impress API",
@@ -490,7 +452,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,

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -44,42 +44,33 @@ msgstr "Me eo an aozer"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Kuzhet"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Korf"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr "Doare korf"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -138,287 +129,291 @@ msgstr "Kleiz"
msgid "Right"
msgstr "Dehoù"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "alc'hwez kentañ evit an enrollañ evel UIID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "krouet d'ar/al"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "deiziad hag eurvezh krouidigezh an enrolladenn"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "hizivaet d'ar/al"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "deiziad hag eurvezh m'eo bet hizivaet an enrolladenn"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "N'hon eus kavet implijer ebet gant an isstrollad-mañ met ar postel a zo liammet ouzh un implijer enrollet."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Ebarzhit un isstrollad mat. An talvoud-mañ a c'hall enderc'hel lizhiri, sifroù hag arouezioù @/./+/-/_/: hepken."
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr "isstrollad"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Rekis. 255 arouezenn pe nebeutoc'h. Lizhiri, sifroù hag arouezioù @/./+/-/_/: hepken."
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "anv klok"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "anv berr"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "postel identelezh"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "postel ar merour"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "yezh"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
msgstr "Ar yezh en deus c'hoant da welet an implijer an etrefas enni."
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
msgstr "Ar gwerzhid-eur en deus c'hoant da welet an implijer an eur drezañ."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "trevnad"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "statud ar skipailh"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "oberiant"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "implijer"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titl"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr "bomm"
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Restr"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
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:967 core/models.py:967
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:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
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:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
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:1191 core/models.py:1191
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:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
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:1363 core/models.py:1363
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:1440 core/models.py:1440
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -138,287 +129,291 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "primärer Schlüssel für den Datensatz als UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "Erstellt"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "Datum und Uhrzeit, an dem ein Datensatz erstellt wurde"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "Aktualisiert"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wir konnten keinen Benutzer mit diesem Abo finden, aber die E-Mail-Adresse ist bereits einem registrierten Benutzer zugeordnet."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr "unter"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr "Auszug"
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
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:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
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:1363 core/models.py:1363
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:1440 core/models.py:1440
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -138,287 +129,291 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr ""
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr ""
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr ""
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1037 core/models.py:1037
#: build/lib/core/models.py:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1362 core/models.py:1362
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Cuerpo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
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:617 core/api/serializers.py:617
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -138,287 +129,291 @@ msgstr "Izquierda"
msgid "Right"
msgstr "Derecha"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "clave primaria para el registro como UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "creado el"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "fecha y hora en la que se creó un registro"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "actualizado el"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "fecha y hora en la que un registro fue actualizado por última vez"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "No se ha podido encontrar un usuario con este sub (UUID), pero el correo electrónico ya está asociado con un usuario."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Introduzca un sub (UUID) válido. Este valor solo puede contener letras, números y los siguientes caracteres @/./+/-/_/:"
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr "sub (UUID)"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Requerido. 255 caracteres o menos. Letras, números y los siguientes caracteres @/./+/-/_/: solamente."
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "nombre completo"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "nombre abreviado"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "correo electrónico de identidad"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "correo electrónico del administrador"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "idioma"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "El idioma en el que el usuario desea ver la interfaz."
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "Si el usuario es un dispositivo o un usuario real."
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "rol en el equipo"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "Si el usuario puede iniciar sesión en esta página web de administración."
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "activo"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si este usuario debe ser considerado como activo. Deseleccionar en lugar de eliminar cuentas."
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "usuario"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "título"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr "resumen"
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
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:967 core/models.py:967
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:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
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:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
msgstr ""
#: build/lib/core/models.py:1271 core/models.py:1271
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
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:1363 core/models.py:1363
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:1440 core/models.py:1440
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:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -44,42 +44,33 @@ msgstr "Je suis l'auteur"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Masqué"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Corps"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
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:617 core/api/serializers.py:617
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -138,287 +129,291 @@ msgstr "Gauche"
msgid "Right"
msgstr "Droite"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr "identifiant/id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "clé primaire pour l'enregistrement en tant que UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "créé le"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "date et heure de création de l'enregistrement"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "mis à jour le"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Nous n'avons pas pu trouver un utilisateur avec ce sous-groupe mais l'e-mail est déjà associé à un utilisateur enregistré."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Saisissez un sous-groupe valide. Cette valeur ne peut contenir que des lettres, des chiffres et les caractères @/./+/-/_/: uniquement."
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr "sous-groupe"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Obligatoire. 255 caractères ou moins. Caractères ASCII uniquement."
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_/: uniquement."
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "nom complet"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "nom court"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "adresse e-mail d'identité"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "adresse e-mail de l'administrateur"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "langue"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "appareil"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "statut d'équipe"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "actif"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "utilisateur"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titre"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr "extrait"
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
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:967 core/models.py:967
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:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
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:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
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:1191 core/models.py:1191
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:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
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:1363 core/models.py:1363
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:1440 core/models.py:1440
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -138,287 +129,291 @@ msgstr "Sinistra"
msgid "Right"
msgstr "Destra"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr "Id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "chiave primaria per il record come UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "creato il"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "data e ora in cui è stato creato un record"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "aggiornato il"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "data e ora in cui lultimo record è stato aggiornato"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Richiesto. 255 caratteri o meno. Solo lettere, numeri e @/./+/-/_/: caratteri."
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "nome completo"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "nome"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "indirizzo email di identità"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "Indirizzo email dell'amministratore"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "lingua"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "Se l'utente è un dispositivo o un utente reale."
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "stato del personale"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "Indica se l'utente può accedere a questo sito amministratore."
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "attivo"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Indica se questo utente deve essere trattato come attivo. Deseleziona invece di eliminare gli account."
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "utente"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titolo"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
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:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
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:1363 core/models.py:1363
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:1440 core/models.py:1440
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Text"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr "Text type"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -138,287 +129,291 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "primaire sleutel voor dossier als UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "gemaakt op"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "datum en tijd wanneer dossier was gecreëerd"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "Laatst gewijzigd op"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "datum en tijd waarop dossier laatst was gewijzigd"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wij konden geen gebruiker vinden met deze id, maar de email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ".Geef een valide id. De waarde mag alleen letters, nummers en @/./.+/-/_: karakters bevatten."
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
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."
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Verplicht. 255 karakters of minder. Alleen letters, nummers en @/./+/-/_/: karakters zijn toegestaan."
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "volledige naam"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "gebruikersnaam"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "identiteit email adres"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "admin email adres"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "taal"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wilt zien."
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker de tijden wilt zien."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "apparaat"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "beheerder status"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen in het admin gedeelte."
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "actief"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "gebruiker"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "titel"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr "uittreksel"
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
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:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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
msgid "Document ask for access"
msgstr "Document verzoekt om toegang"
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
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:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
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:1363 core/models.py:1363
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:1440 core/models.py:1440
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
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:617 core/api/serializers.py:617
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -138,287 +129,291 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr ""
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr ""
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr ""
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1037 core/models.py:1037
#: build/lib/core/models.py:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1362 core/models.py:1362
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -1,426 +0,0 @@
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"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: ru\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Личная информация"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Разрешения"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Древовидная структура"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Создатель - я"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Скрытый"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
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
msgid "Body"
msgstr "Текст сообщения"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Тип сообщения"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Читатель"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Редактор"
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Администратор"
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Владелец"
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Доступ ограничен"
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Аутентификация выполнена"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Доступно всем"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Первый потомок"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Последний потомок"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Первый предок"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Последний предок"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Слева"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Справа"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "первичный ключ для записи как UUID"
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "создано"
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "дата и время создания записи"
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "обновлено"
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "дата и время последнего обновления записи"
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Мы не смогли найти пользователя с этими данными, но этот адрес уже связан с зарегистрированным пользователем."
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "вложение"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Обязательно. 255 символов или меньше. Только ASCII символы."
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "полное имя"
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "короткое имя"
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "личный адрес электронной почты"
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "e-mail администратора"
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "язык"
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "устройство"
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Пользователь является устройством или человеком."
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "статус сотрудника"
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Может ли пользователь войти на этот административный сайт."
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "активный"
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "пользователь"
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "отрывок"
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1002 core/models.py:1002
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
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1037 core/models.py:1037
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
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1271 core/models.py:1271
#, 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
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип email"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Открыть"
#: core/templates/mail/html/template.html:217
#: 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/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Доступ получен от %(brandname)s "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "Telo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr "Vrsta telesa"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -138,287 +129,291 @@ msgstr "Levo"
msgid "Right"
msgstr "Desno"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "primarni ključ za zapis kot UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "ustvarjen na"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "datum in čas, ko je bil zapis ustvarjen"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "posodobljeno dne"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "datum in čas, ko je bil zapis nazadnje posodobljen"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Nismo mogli najti uporabnika s tem sub, vendar je e-poštni naslov že povezan z registriranim uporabnikom."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Vnesite veljavno sub. Ta vrednost lahko vsebuje samo črke, številke in znake @/./+/-/_/:."
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Obvezno. 255 znakov ali manj. Samo črke, številke in znaki @/./+/-/_/: ."
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "polno ime"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "kratko ime"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "elektronski naslov identitete"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "elektronski naslov skrbnika"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "jezik"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "naprava"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "kadrovski status"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "aktivni"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ali je treba tega uporabnika obravnavati kot aktivnega. Namesto brisanja računov počistite to izbiro."
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "uporabnik"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "naslov"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr "odlomek"
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
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:967 core/models.py:967
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:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
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:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
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:1357 core/models.py:1357
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:1363 core/models.py:1363
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:1440 core/models.py:1440
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -138,287 +129,291 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "aktiv"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr ""
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr ""
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1037 core/models.py:1037
#: build/lib/core/models.py:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1362 core/models.py:1362
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -138,287 +129,291 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr ""
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr ""
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr ""
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr ""
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1037 core/models.py:1037
#: build/lib/core/models.py:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
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:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1190 core/models.py:1190
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1362 core/models.py:1362
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -1,426 +0,0 @@
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"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: uk\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Дозволи"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Ієрархічна структура"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Творець — я"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Приховано"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
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
msgid "Body"
msgstr "Вміст"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Тип вмісту"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Читач"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Редактор"
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Адміністратор"
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Власник"
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Обмежено"
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Підтверджено"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Публічне"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Перший нащадок"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Останній нащадок"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Перший пращур"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Останній пращур"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Ліворуч"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Праворуч"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "первинний ключ для запису як UUID"
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "створено"
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "дата і час, коли запис було створено"
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "оновлено"
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "дата і час, коли запис був востаннє оновлений"
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Ми не змогли знайти користувача з цими даними, але адреса вже пов'язана з зареєстрованим користувачем."
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "вкладений документ"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Обов'язкове. 255 символів або менше. Тільки символи ASCII."
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "повне ім'я"
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "коротке ім'я"
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "адреса електронної пошти особи"
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "електронна адреса адміністратора"
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "мова"
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Мова, якою користувач хоче бачити інтерфейс."
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Часовий пояс, в якому користувач хоче бачити час."
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "пристрій"
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Чи є користувач пристроєм чи реальним користувачем."
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "статус співробітника"
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Чи може користувач увійти на цей сайт адміністратора."
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "активний"
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "користувач"
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "уривок"
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1002 core/models.py:1002
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
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1037 core/models.py:1037
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
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1271 core/models.py:1271
#, 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
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип пошти"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Відкрити"
#: core/templates/mail/html/template.html:217
#: 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/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Запрошення отримане від %(brandname)s "

View File

@@ -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-07-24 20:42+0000\n"
"PO-Revision-Date: 2025-07-31 12:38\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:467 core/api/serializers.py:467
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:471 core/api/serializers.py:471
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:608 core/api/serializers.py:608
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -138,287 +129,291 @@ msgstr "左"
msgid "Right"
msgstr "右"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:79 core/models.py:79
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "primary key for the record as UUID"
msgstr "记录的主密钥为 UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:86 core/models.py:86
msgid "created on"
msgstr "创建时间"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "date and time at which a record was created"
msgstr "记录的创建日期和时间"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:92 core/models.py:92
msgid "updated on"
msgstr "更新时间"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "date and time at which a record was last updated"
msgstr "记录的最后更新时间"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:129 core/models.py:129
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "未找到具有该 sub 的用户,但该邮箱已关联到一个注册用户。"
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "请输入有效的 sub。该值只能包含字母、数字及 @/./+/-/_/: 字符。"
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgstr "sub"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "必填。最多 255 个字符,仅允许字母、数字及 @/./+/-/_/: 字符。"
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "全名"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:160 core/models.py:160
msgid "short name"
msgstr "简称"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "identity email address"
msgstr "身份电子邮件地址"
#: build/lib/core/models.py:158 core/models.py:158
#: build/lib/core/models.py:167 core/models.py:167
msgid "admin email address"
msgstr "管理员电子邮件地址"
#: build/lib/core/models.py:165 core/models.py:165
#: build/lib/core/models.py:174 core/models.py:174
msgid "language"
msgstr "语言"
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "The language in which the user wants to see the interface."
msgstr "用户希望看到的界面语言。"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "The timezone in which the user wants to see times."
msgstr "用户查看时间希望的时区。"
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:186 core/models.py:186
msgid "device"
msgstr "设备"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:188 core/models.py:188
msgid "Whether the user is a device or a real user."
msgstr "用户是设备还是真实用户。"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:191 core/models.py:191
msgid "staff status"
msgstr "员工状态"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether the user can log into this admin site."
msgstr "用户是否可以登录该管理员站点。"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:196 core/models.py:196
msgid "active"
msgstr "激活"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
#: build/lib/core/models.py:202 core/models.py:202
#: build/lib/core/models.py:211 core/models.py:211
msgid "user"
msgstr "用户"
#: build/lib/core/models.py:203 core/models.py:203
#: build/lib/core/models.py:212 core/models.py:212
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:368 build/lib/core/models.py:1283
#: core/models.py:368 core/models.py:1283
msgid "title"
msgstr "标题"
#: build/lib/core/models.py:360 core/models.py:360
#: build/lib/core/models.py:369 core/models.py:369
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:409 core/models.py:409
#: build/lib/core/models.py:418 core/models.py:418
msgid "Document"
msgstr "文档"
#: build/lib/core/models.py:410 core/models.py:410
#: build/lib/core/models.py:419 core/models.py:419
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:431 build/lib/core/models.py:821 core/models.py:431
#: core/models.py:821
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:856 core/models.py:856
#, 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:860 core/models.py:860
#, 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:866 core/models.py:866
#, 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:966 core/models.py:966
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:972 core/models.py:972
#: build/lib/core/models.py:973 core/models.py:973
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:996 core/models.py:996
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:996 core/models.py:996
#: build/lib/core/models.py:997 core/models.py:997
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1002 core/models.py:1002
#: build/lib/core/models.py:1003 core/models.py:1003
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:1025 core/models.py:1025
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1025 core/models.py:1025
#: build/lib/core/models.py:1026 core/models.py:1026
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1031 core/models.py:1031
#: build/lib/core/models.py:1032 core/models.py:1032
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1037 core/models.py:1037
#: build/lib/core/models.py:1038 core/models.py:1038
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:1044 build/lib/core/models.py:1369
#: core/models.py:1044 core/models.py:1369
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr "文档需要访问权限"
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
msgid "Document ask for accesses"
msgstr "文档需要访问权限"
msgstr ""
#: build/lib/core/models.py:1196 core/models.py:1196
#: build/lib/core/models.py:1197 core/models.py:1197
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:1262 core/models.py:1262
#, 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:1266 core/models.py:1266
#, 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:1272 core/models.py:1272
#, 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:1284 core/models.py:1284
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1290 core/models.py:1290
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:1296 core/models.py:1296
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1296 core/models.py:1296
#: build/lib/core/models.py:1297 core/models.py:1297
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1350 core/models.py:1350
#: build/lib/core/models.py:1351 core/models.py:1351
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1356 core/models.py:1356
#: build/lib/core/models.py:1357 core/models.py:1357
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1362 core/models.py:1362
#: build/lib/core/models.py:1363 core/models.py:1363
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1439 core/models.py:1439
#: build/lib/core/models.py:1440 core/models.py:1440
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1459 core/models.py:1459
#: build/lib/core/models.py:1460 core/models.py:1460
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1479 core/models.py:1479
#: build/lib/core/models.py:1480 core/models.py:1480
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 "

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.8.1"
version = "3.5.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -29,17 +29,18 @@ dependencies = [
"boto3==1.39.4",
"Brotli==1.1.0",
"celery[redis]==5.5.3",
"dj-database-url==2.3.0",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-csp==4.0",
"django-filter==25.1",
"django-lasuite[all]==0.0.14",
"django-lasuite[all]==0.0.11",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.2.7",
"django==5.2.4",
"django-treebeard==4.7.1",
"djangorestframework==3.16.0",
"drf_spectacular==0.28.0",

View File

@@ -10,13 +10,13 @@ WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json
COPY ./src/frontend/yarn.lock ./yarn.lock
COPY ./src/frontend/apps/impress/package.json ./apps/impress/package.json
COPY ./src/frontend/packages/eslint-plugin-docs/package.json ./packages/eslint-plugin-docs/package.json
COPY ./src/frontend/packages/eslint-config-impress/package.json ./packages/eslint-config-impress/package.json
RUN yarn install --frozen-lockfile
COPY .dockerignore ./.dockerignore
COPY ./src/frontend/.prettierrc.js ./.prettierrc.js
COPY ./src/frontend/packages/eslint-plugin-docs ./packages/eslint-plugin-docs
COPY ./src/frontend/packages/eslint-config-impress ./packages/eslint-config-impress
COPY ./src/frontend/apps/impress ./apps/impress
### ---- Front-end builder image ----
@@ -50,13 +50,7 @@ ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
RUN yarn build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.22 AS frontend-production
# Upgrade system packages to install security updates
USER root
RUN apk update && \
apk upgrade && \
rm -rf /var/cache/apk/*
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
# Un-privileged user running the application
ARG DOCKER_USER

View File

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

View File

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

View File

@@ -43,14 +43,12 @@ test.describe('Config', () => {
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
await expect(image).toBeVisible();
// Wait for the media-check to be processed
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
// Check src of image

View File

@@ -45,8 +45,8 @@ test.describe('Doc Create', () => {
})
.click();
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('', { timeout: 10000 });
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
@@ -67,8 +67,8 @@ test.describe('Doc Create', () => {
.getByText('New sub-doc')
.click();
const input = page.getByRole('textbox', { name: 'Document title' });
await expect(input).toHaveText('', { timeout: 10000 });
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
@@ -89,8 +89,8 @@ test.describe('Doc Create: Not logged', () => {
const data = {
title,
content: markdown,
sub: `user.test@${browserName}.test`,
email: `user.test@${browserName}.test`,
sub: `user@${browserName}.test`,
email: `user@${browserName}.test`,
};
const newDoc = await request.post(

View File

@@ -1,7 +1,6 @@
/* 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 {
@@ -11,9 +10,7 @@ import {
overrideConfig,
verifyDocName,
} from './utils-common';
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
import { createRootSubPage, navigateToPageFromTree } from './utils-sub-pages';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -88,20 +85,21 @@ test.describe('Doc Editor', () => {
// Is connected
let framesentPromise = webSocket.waitForEvent('framesent');
await writeInEditor({ page, text: 'Hello World' });
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
let framesent = await framesentPromise;
expect(framesent.payload).not.toBeNull();
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
// When the visibility is changed, the ws should close the connection (backend signal)
const wsClosePromise = webSocket.waitForEvent('close');
await selectVisibility.click();
await page.getByRole('menuitem', { name: 'Connected' }).click();
await page.getByLabel('Connected').click();
// Assert that the doc reconnects to the ws
const wsClose = await wsClosePromise;
@@ -216,6 +214,7 @@ test.describe('Doc Editor', () => {
});
test('it saves the doc when we quit pages', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(browserName === 'webkit', 'This test is very flaky with webkit');
// Check the first doc
@@ -227,19 +226,25 @@ test.describe('Doc Editor', () => {
await editor.fill('Hello World Doc persisted 2');
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
await page.waitForTimeout(1000);
const urlDoc = page.url();
await page.goto(urlDoc);
// Wait for editor to load
await expect(editor).toBeVisible();
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
});
test('it cannot edit if viewer', async ({ page }) => {
await mockedDocument(page, {
user_role: 'reader',
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
accesses_manage: false, // Means not admin
update: false,
partial_update: false, // Means not editor
retrieve: true,
},
});
await goToGridDoc(page);
@@ -248,9 +253,6 @@ test.describe('Doc Editor', () => {
await expect(card).toBeVisible();
await expect(card.getByText('Reader')).toBeVisible();
const editor = page.locator('.ProseMirror');
await expect(editor).toHaveAttribute('contenteditable', 'false');
});
test('it adds an image to the doc editor', async ({ page, browserName }) => {
@@ -270,25 +272,18 @@ test.describe('Doc Editor', () => {
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
await expect(image).toBeVisible();
// Wait for the media-check to be processed
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
// Check src of image
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
await expect(image).toHaveAttribute('role', 'presentation');
await expect(image).toHaveAttribute('alt', '');
await expect(image).toHaveAttribute('tabindex', '-1');
await expect(image).toHaveAttribute('aria-hidden', 'true');
});
test('it checks the AI buttons', async ({ page, browserName }) => {
@@ -395,6 +390,8 @@ test.describe('Doc Editor', () => {
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
return;
@@ -421,6 +418,8 @@ test.describe('Doc Editor', () => {
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
/* eslint-enable playwright/no-conditional-expect */
/* eslint-enable playwright/no-conditional-in-test */
});
});
@@ -461,14 +460,12 @@ test.describe('Doc Editor', () => {
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
@@ -506,7 +503,10 @@ test.describe('Doc Editor', () => {
await verifyDocName(page, randomDoc);
const editor = await openSuggestionMenu({ page });
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Embedded file').click();
await page.getByText('Upload file').click();
@@ -561,7 +561,20 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
await updateShareLink(page, 'Public', 'Editing');
await page.getByLabel('Visibility', { exact: true }).click();
await page
.getByRole('menuitem', {
name: 'Public',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByLabel('Visibility mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
@@ -585,12 +598,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,18 +649,13 @@ 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);
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Visibility mode').click();
await page.getByRole('menuitem', { name: 'Reading' }).click();
// Close the modal
@@ -652,11 +665,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();
@@ -665,7 +673,9 @@ test.describe('Doc Editor', () => {
test('it checks if callout custom block', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
await openSuggestionMenu({ page });
const editor = page.locator('.ProseMirror');
await editor.click();
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Add a callout block').click();
const calloutBlock = page
@@ -684,20 +694,8 @@ test.describe('Doc Editor', () => {
const emojiButton = calloutBlock.getByRole('button');
await expect(emojiButton).toHaveText('💡');
await emojiButton.click();
// Group smiley
await expect(page.getByRole('button', { name: '🤠' })).toBeVisible();
// Group animals
await page.getByText('Animals & Nature').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🦆' })).toBeVisible();
// Group travel
await page.getByText('Travel & Places').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🚝' })).toBeVisible();
// Group objects
await page.getByText('Objects').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🪇' })).toBeVisible();
// Group symbol
await page.getByText('Symbols').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🛃' })).toBeVisible();
await page.locator('button[aria-label="⚠️"]').click();
await expect(emojiButton).toHaveText('⚠️');
await page.locator('.bn-side-menu > button').last().click();
await page.locator('.mantine-Menu-dropdown > button').last().click();
@@ -750,21 +748,15 @@ test.describe('Doc Editor', () => {
await expect(searchContainer.getByText(docChild2)).toBeVisible();
await expect(searchContainer.getByText(randomDoc)).toBeHidden();
await page.keyboard.press('ArrowDown');
// use keydown to select the second result
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
// Wait for the search container to disappear, indicating selection was made
await expect(searchContainer).toBeHidden();
// Wait for the interlink to be created and rendered
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('button', {
name: docChild2,
const interlink = page.getByRole('link', {
name: 'child-2',
});
await expect(interlink).toBeVisible({ timeout: 10000 });
await expect(interlink).toBeVisible();
await interlink.click();
await verifyDocName(page, docChild2);
@@ -785,93 +777,4 @@ test.describe('Doc Editor', () => {
),
).toBeVisible();
});
test('it checks multiple big doc scroll to the top', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(page, 'doc-scroll', browserName, 1);
for (let i = 0; i < 15; i++) {
await page.keyboard.press('Enter');
await writeInEditor({ page, text: 'Hello Parent ' + i });
}
const editor = await getEditor({ page });
await expect(
editor.getByText('Hello Parent 1', { exact: true }),
).not.toBeInViewport();
await expect(editor.getByText('Hello Parent 14')).toBeInViewport();
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-scroll-child',
);
for (let i = 0; i < 15; i++) {
await page.keyboard.press('Enter');
await writeInEditor({ page, text: 'Hello Child ' + i });
}
await expect(
editor.getByText('Hello Child 1', { exact: true }),
).not.toBeInViewport();
await expect(editor.getByText('Hello Child 14')).toBeInViewport();
await navigateToPageFromTree({ page, title: randomDoc });
await expect(
editor.getByText('Hello Parent 1', { exact: true }),
).toBeInViewport();
await expect(editor.getByText('Hello Parent 14')).not.toBeInViewport();
await navigateToPageFromTree({ page, title: docChild });
await expect(
editor.getByText('Hello Child 1', { exact: true }),
).toBeInViewport();
await expect(editor.getByText('Hello Child 14')).not.toBeInViewport();
});
test('it embeds PDF', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
await openSuggestionMenu({ page });
await page.getByText('Embed a PDF file').click();
const pdfBlock = page.locator('div[data-content-type="pdf"]').first();
await expect(pdfBlock).toBeVisible();
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;
await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf'));
// Wait for the media-check to be processed
await page.waitForTimeout(1000);
const pdfEmbed = page
.locator('.--docs--editor-container embed.bn-visual-media')
.first();
// Check src of pdf
expect(await pdfEmbed.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.pdf/,
);
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');
});
});

View File

@@ -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 }) => {
@@ -26,11 +25,16 @@ test.describe('Doc Export', () => {
await createDoc(page, 'doc-editor', browserName, 1);
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
})
.click();
await expect(page.getByTestId('modal-export-title')).toBeVisible();
await expect(
page
.locator('div')
.filter({ hasText: /^Download$/ })
.first(),
).toBeVisible();
await expect(
page.getByText('Download your document in a .docx or .pdf format.'),
).toBeVisible();
@@ -39,11 +43,9 @@ test.describe('Doc Export', () => {
).toBeVisible();
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Close the download modal',
}),
page.getByRole('button', { name: 'Close the modal' }),
).toBeVisible();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
});
test('it exports the doc with pdf line break', async ({
@@ -76,7 +78,8 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
exact: true,
})
.click();
@@ -84,7 +87,12 @@ test.describe('Doc Export', () => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -94,7 +102,6 @@ test.describe('Doc Export', () => {
expect(pdfData.numpages).toBe(2);
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
expect(pdfData.info.Title).toBe(randomDoc);
});
test('it exports the doc to docx', async ({ page, browserName }) => {
@@ -115,28 +122,37 @@ test.describe('Doc Export', () => {
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
const image = page.getByRole('img', { name: 'test.svg' });
await expect(image).toBeVisible();
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
exact: true,
})
.click();
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.docx`);
});
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
@@ -154,13 +170,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');
@@ -168,9 +182,7 @@ test.describe('Doc Export', () => {
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
const image = page.getByRole('img', { name: 'test.svg' });
await expect(image).toBeVisible();
@@ -184,7 +196,7 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
})
.click();
@@ -204,7 +216,11 @@ test.describe('Doc Export', () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Download',
}),
).toBeVisible();
const responseCorsPromise = page.waitForResponse(
(response) =>
@@ -215,7 +231,11 @@ test.describe('Doc Export', () => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Download',
})
.click();
const responseCors = await responseCorsPromise;
expect(responseCors.ok()).toBe(true);
@@ -253,17 +273,25 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
})
.click();
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Download',
}),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Download',
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -295,19 +323,28 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
exact: true,
})
.click();
await expect(
page.getByTestId('doc-open-modal-download-button'),
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -349,19 +386,28 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
exact: true,
})
.click();
await expect(
page.getByTestId('doc-open-modal-download-button'),
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
@@ -397,9 +443,14 @@ test.describe('Doc Export', () => {
})
.click();
const input = page.getByRole('textbox', { name: 'Titre du document' });
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'domcontentloaded',
});
const input = page.getByLabel('doc title input');
await expect(input).toBeVisible();
await expect(input).toHaveText('', { timeout: 10000 });
await expect(input).toHaveText('');
await input.click();
await input.fill(randomDocFrench);
await input.blur();
@@ -410,19 +461,21 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'Exporter le document',
name: 'download',
exact: true,
})
.click();
await expect(
page.getByTestId('doc-open-modal-download-button'),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Télécharger',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
@@ -457,23 +510,19 @@ test.describe('Doc Export', () => {
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
const input = page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
);
const searchContainer = page.locator('.quick-search-container');
await page
.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
)
.fill('interlink-child');
await input.fill('export-interlink');
await page
.locator('.quick-search-container')
.getByText('interlink-child')
.click();
await expect(searchContainer).toBeVisible();
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
// We are in docChild, we want to create a link to randomDoc (parent)
await searchContainer.getByText(randomDoc).click();
// Search the interlinking link in the editor (not in the document tree)
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('button', {
name: randomDoc,
const interlink = page.getByRole('link', {
name: 'interlink-child',
});
await expect(interlink).toBeVisible();
@@ -484,11 +533,17 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'Export the document',
name: 'download',
exact: true,
})
.click();
void page.getByTestId('doc-export-download-button').click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
@@ -496,6 +551,6 @@ test.describe('Doc Export', () => {
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain(randomDoc);
expect(pdfData.text).toContain('interlink-child'); // This is the pdf text
});
});

View File

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

View File

@@ -28,7 +28,7 @@ test.describe('Documents Grid mobile', () => {
id: '8c1e047a-24e7-4a80-942b-8e9c7ab43e1f',
user: {
id: '7380f42f-02eb-4ad5-b8f0-037a0e66066d',
email: 'test.test@test.test',
email: 'test@test.test',
full_name: 'John Doe',
short_name: 'John',
},
@@ -80,7 +80,9 @@ test.describe('Documents Grid mobile', () => {
hasText: 'My mocked document',
});
await expect(row.getByTestId('doc-title')).toHaveText('My mocked document');
await expect(
row.locator('[aria-describedby="doc-title"]').nth(0),
).toHaveText('My mocked document');
});
});
@@ -117,7 +119,7 @@ test.describe('Document grid item options', () => {
await page.getByText('push_pin').click();
// Check is pinned
await expect(row.getByTestId('doc-pinned-icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -126,7 +128,7 @@ test.describe('Document grid item options', () => {
await page.getByText('Unpin').click();
// Check is unpinned
await expect(row.getByTestId('doc-pinned-icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -139,7 +141,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' }),
@@ -147,7 +149,7 @@ test.describe('Document grid item options', () => {
await page
.getByRole('button', {
name: 'Delete document',
name: 'Confirm deletion',
})
.click();
@@ -293,7 +295,7 @@ test.describe('Documents Grid', () => {
docs = result.results as SmallDoc[];
await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(page.locator('h2').getByText('All docs')).toBeVisible();
await expect(page.locator('h4').getByText('All docs')).toBeVisible();
const thead = page.getByTestId('docs-grid-header');
await expect(thead.getByText(/Name/i)).toBeVisible();

View File

@@ -25,12 +25,12 @@ test.describe('Doc Header', () => {
'It is the card information about the document.',
);
const docTitle = card.getByRole('textbox', { name: 'Document title' });
const docTitle = card.getByRole('textbox', { name: 'doc title input' });
await expect(docTitle).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-visibility').click();
await page.getByLabel('Visibility', { exact: true }).click();
await page
.getByRole('menuitem', {
@@ -44,9 +44,7 @@ test.describe('Doc Header', () => {
await expect(card.getByText('Owner ·')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Open the document options' }),
).toBeVisible();
@@ -54,53 +52,28 @@ test.describe('Doc Header', () => {
test('it updates the title doc', async ({ page, browserName }) => {
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'Document title' });
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
await expect(docTitle).toBeVisible();
await docTitle.fill('Hello World');
await docTitle.blur();
await verifyDocName(page, 'Hello World');
});
test('it updates the title doc adding a leading emoji', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await expect(docTitle).toBeVisible();
await docTitle.fill('👍 Hello Emoji World');
await docTitle.blur();
await verifyDocName(page, '👍 Hello Emoji World');
// Check the tree
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText('Hello Emoji World')).toBeVisible();
await expect(docTree.getByTestId('doc-emoji-icon')).toBeVisible();
await expect(docTree.getByTestId('doc-simple-icon')).toBeHidden();
await page.getByTestId('home-button').click();
// Check the documents grid
const gridRow = await getGridRow(page, 'Hello Emoji World');
await expect(gridRow.getByTestId('doc-emoji-icon')).toBeVisible();
await expect(gridRow.getByTestId('doc-simple-icon')).toBeHidden();
});
test('it deletes the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Delete document' }).click();
await page.getByLabel('Delete document').click();
await expect(
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', {
name: 'Delete document',
name: 'Confirm deletion',
})
.click();
@@ -142,35 +115,25 @@ 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();
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
await expect(page.getByLabel('Delete document')).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Share' }).click();
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
const shareModal = page.getByLabel('Share modal');
await expect(shareModal).toBeVisible();
await expect(page.getByText('Share the document')).toBeVisible();
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test.test@invitation.test').first(),
invitationCard.getByText('test@invitation.test').first(),
).toBeVisible();
const invitationRole = invitationCard.getByLabel('doc-role-dropdown');
await expect(invitationRole).toBeVisible();
@@ -184,7 +147,7 @@ test.describe('Doc Header', () => {
const roles = memberCard.getByLabel('doc-role-dropdown');
await expect(memberCard).toBeVisible();
await expect(
memberCard.getByText('test.test@accesses.test').first(),
memberCard.getByText('test@accesses.test').first(),
).toBeVisible();
await expect(roles).toBeVisible();
@@ -222,45 +185,33 @@ 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();
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
await expect(page.getByLabel('Delete document')).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Share' }).click();
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
await expect(shareModal).toBeVisible();
const shareModal = page.getByLabel('Share modal');
await expect(page.getByText('Share the document')).toBeVisible();
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test.test@invitation.test').first(),
invitationCard.getByText('test@invitation.test').first(),
).toBeVisible();
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
await expect(
invitationCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
await expect(memberCard.getByText('test.test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
await expect(
memberCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
@@ -294,18 +245,10 @@ 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();
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
await expect(page.getByLabel('Delete document')).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
@@ -318,18 +261,17 @@ test.describe('Doc Header', () => {
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test.test@invitation.test').first(),
invitationCard.getByText('test@invitation.test').first(),
).toBeVisible();
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
await expect(
invitationCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
await expect(memberCard.getByText('test.test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
await expect(
memberCard.getByRole('button', { name: 'more_horiz' }),
).toBeHidden();
@@ -339,6 +281,7 @@ test.describe('Doc Header', () => {
page,
browserName,
}) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -361,7 +304,7 @@ test.describe('Doc Header', () => {
// Copy content to clipboard
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Copy as Markdown' }).click();
await page.getByLabel('Copy as Markdown').click();
await expect(page.getByText('Copied to clipboard')).toBeVisible();
// Test that clipboard is in Markdown format
@@ -373,6 +316,7 @@ test.describe('Doc Header', () => {
});
test('It checks the copy as HTML button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -395,7 +339,7 @@ test.describe('Doc Header', () => {
// Copy content to clipboard
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Copy as HTML' }).click();
await page.getByLabel('Copy as HTML').click();
await expect(page.getByText('Copied to clipboard')).toBeVisible();
// Test that clipboard is in HTML format
@@ -407,6 +351,7 @@ test.describe('Doc Header', () => {
});
test('it checks the copy link button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -452,15 +397,11 @@ test.describe('Doc Header', () => {
test('it pins a document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `Pin doc`, browserName);
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await page.getByLabel('Open the document options').click();
// Pin
await page.getByText('push_pin').click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await page.getByLabel('Open the document options').click();
await expect(page.getByText('Unpin')).toBeVisible();
await page.goto('/');
@@ -468,26 +409,22 @@ test.describe('Doc Header', () => {
const row = await getGridRow(page, docTitle);
// Check is pinned
await expect(row.getByTestId('doc-pinned-icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
await row.getByText(docTitle).click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await page.getByLabel('Open the document options').click();
// Unpin
await page.getByText('Unpin').click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await page.getByLabel('Open the document options').click();
await expect(page.getByText('push_pin')).toBeVisible();
await page.goto('/');
// Check is unpinned
await expect(row.getByTestId('doc-pinned-icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -586,7 +523,7 @@ test.describe('Documents Header mobile', () => {
await expect(
page.getByRole('menuitem', { name: 'Copy link' }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'Share' }).click();
await page.getByLabel('Share').click();
await expect(page.getByRole('button', { name: 'Copy link' })).toBeVisible();
});
@@ -609,12 +546,9 @@ test.describe('Documents Header mobile', () => {
await goToGridDoc(page);
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Share' }).click();
await page.getByLabel('Share').click();
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
});
await expect(shareModal).toBeVisible();
await expect(page.getByLabel('Share modal')).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
await expect(page.getByLabel('Share modal')).toBeHidden();
});

View File

@@ -18,7 +18,7 @@ test.describe('Inherited share accesses', () => {
).toBeVisible();
const user = page.getByTestId(
`doc-share-member-row-user.test@${browserName}.test`,
`doc-share-member-row-user@${browserName}.test`,
);
await expect(user).toBeVisible();
await expect(user.getByText('E2E Chromium')).toBeVisible();
@@ -52,12 +52,6 @@ test.describe('Inherited share accesses', () => {
await expect(docVisibilityCard.getByText('Connected')).toBeVisible();
await expect(docVisibilityCard.getByText('Reading')).toBeVisible();
await docVisibilityCard.getByText('Reading').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await expect(docVisibilityCard.getByText('Reading')).toBeHidden();
await expect(docVisibilityCard.getByText('Editing')).toBeVisible();
// Verify inherited link
await docVisibilityCard.getByText('Connected').click();
await expect(
@@ -67,13 +61,17 @@ test.describe('Inherited share accesses', () => {
// Update child link
await page.getByRole('menuitem', { name: 'Public' }).click();
await docVisibilityCard.getByText('Reading').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await expect(docVisibilityCard.getByText('Connected')).toBeHidden();
await expect(docVisibilityCard.getByText('Reading')).toBeHidden();
await expect(
docVisibilityCard.getByText('Public', {
exact: true,
}),
).toBeVisible();
await expect(docVisibilityCard.getByText('Editing')).toBeVisible();
await expect(
docVisibilityCard.getByText(
'The link sharing rules differ from the parent document',

View File

@@ -7,7 +7,6 @@ import {
randomName,
verifyDocName,
} from './utils-common';
import { connectOtherUserToDoc, updateRoleUser } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Document create member', () => {
@@ -16,7 +15,7 @@ test.describe('Document create member', () => {
});
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
const inputFill = 'user.test';
const inputFill = 'user ';
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes(`/users/?q=${encodeURIComponent(inputFill)}`) &&
@@ -26,8 +25,9 @@ test.describe('Document create member', () => {
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByTestId('quick-search-input');
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
await expect(inputSearch).toBeVisible();
// Select user 1 and verify tag
@@ -74,15 +74,13 @@ test.describe('Document create member', () => {
// Check roles are displayed
await list.getByLabel('doc-role-dropdown').click();
await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Administrator' }),
).toBeVisible();
await expect(page.getByLabel('Reader')).toBeVisible();
await expect(page.getByLabel('Editor')).toBeVisible();
await expect(page.getByLabel('Owner')).toBeVisible();
await expect(page.getByLabel('Administrator')).toBeVisible();
// Validate
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByLabel('Administrator').click();
await page.getByRole('button', { name: 'Invite' }).click();
// Check invitation added
@@ -119,7 +117,9 @@ test.describe('Document create member', () => {
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByTestId('quick-search-input');
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
const [email] = randomName('test@test.fr', browserName, 1);
await inputSearch.fill(email);
@@ -128,7 +128,7 @@ test.describe('Document create member', () => {
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Owner' }).click();
await page.getByLabel('Owner').click();
const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
@@ -146,7 +146,7 @@ test.describe('Document create member', () => {
// Choose a role
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Owner' }).click();
await page.getByLabel('Owner').click();
const responsePromiseCreateInvitationFail = page.waitForResponse(
(response) =>
@@ -167,23 +167,18 @@ test.describe('Document create member', () => {
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByTestId('quick-search-input');
const inputSearch = page.getByRole('combobox', {
name: '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
const container = page.getByTestId('doc-share-add-member-list');
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByLabel('Administrator').click();
const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
@@ -198,32 +193,26 @@ 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();
await page.getByLabel('Reader').click();
const moreActions = userInvitation.getByRole('button', {
name: 'Open invitation actions menu',
name: 'more_horiz',
});
await moreActions.click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByLabel('Delete').click();
await expect(userInvitation).toBeHidden();
});
});
test.describe('Document create member: Multiple login', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('It creates a member from a request coming from a 403 page', async ({
page,
@@ -231,6 +220,9 @@ test.describe('Document create member', () => {
}) => {
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
page,
'Member access request',
@@ -240,67 +232,67 @@ test.describe('Document create member', () => {
await verifyDocName(page, docTitle);
const urlDoc = page.url();
await page
.locator('.ProseMirror')
.locator('.bn-block-outer')
.last()
.fill('Hello World');
.getByRole('button', {
name: 'Logout',
})
.click();
const docUrl = page.url();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
// Other user will request access
const { otherPage, otherBrowserName, cleanup } =
await connectOtherUserToDoc({ browserName, docUrl });
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),
page.getByText('Insufficient access rights to view the document.'),
).toBeVisible({
timeout: 10000,
});
await otherPage.getByRole('button', { name: 'Request access' }).click();
await page.getByRole('button', { name: 'Request access' }).click();
await expect(
otherPage.getByText('Your access request for this document is pending.'),
page.getByText('Your access request for this document is pending.'),
).toBeVisible();
// First user approves the request
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await page.goto('/');
await keyCloakSignIn(page, browserName);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
await page.goto(urlDoc);
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByText('Access Requests')).toBeVisible();
await expect(page.getByText(`E2E ${otherBrowserName}`)).toBeVisible();
await expect(page.getByText(`E2E ${otherBrowser}`)).toBeVisible();
const emailRequest = `user.test@${otherBrowserName}.test`;
const emailRequest = `user@${otherBrowser}.test`;
await expect(page.getByText(emailRequest)).toBeVisible();
const container = page.getByTestId(
`doc-share-access-request-row-${emailRequest}`,
);
await container.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByLabel('Administrator').click();
await container.getByRole('button', { name: 'Approve' }).click();
await expect(page.getByText('Access Requests')).toBeHidden();
await expect(page.getByText('Share with 2 users')).toBeVisible();
await expect(page.getByText(`E2E ${otherBrowserName}`)).toBeVisible();
// Other user verifies he has access
await otherPage.reload();
await verifyDocName(otherPage, docTitle);
await expect(otherPage.getByText('Hello World')).toBeVisible();
// Revoke access
await updateRoleUser(page, 'Remove access', emailRequest);
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),
).toBeVisible();
// Cleanup: other user logout
await cleanup();
await expect(page.getByText(`E2E ${otherBrowser}`)).toBeVisible();
});
});
test.describe('Document create member: Multiple login', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('It cannot request member access on child doc on a 403 page', async ({
page,

View File

@@ -139,7 +139,7 @@ test.describe('Document list members', () => {
const list = page.getByTestId('doc-share-quick-search');
await expect(list).toBeVisible();
const currentUser = list.getByTestId(
`doc-share-member-row-user.test@${browserName}.test`,
`doc-share-member-row-user@${browserName}.test`,
);
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await expect(currentUser).toBeVisible();
@@ -151,6 +151,7 @@ test.describe('Document list members', () => {
await expect(soloOwner).toBeVisible();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
const newUserEmail = await addNewMember(page, 0, 'Owner');
@@ -162,21 +163,23 @@ test.describe('Document list members', () => {
await currentUserRole.click();
await expect(soloOwner).toBeHidden();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
await newUserRoles.click();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
await currentUserRole.click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByLabel('Administrator').click();
await list.click();
await expect(currentUserRole).toBeVisible();
await currentUserRole.click();
await page.getByRole('menuitem', { name: 'Reader' }).click();
await page.getByLabel('Reader').click();
await list.click();
await expect(currentUserRole).toBeHidden();
});
@@ -190,7 +193,7 @@ test.describe('Document list members', () => {
const list = page.getByTestId('doc-share-quick-search');
const emailMyself = `user.test@${browserName}.test`;
const emailMyself = `user@${browserName}.test`;
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
const mySelfRole = mySelf.getByRole('button', {
name: 'doc-role-dropdown',

View File

@@ -40,6 +40,7 @@ test.describe('Doc Routing', () => {
});
test('checks 404 on docs/[id] page', async ({ page }) => {
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(300);
await page.goto('/docs/some-unknown-doc');
@@ -85,15 +86,7 @@ test.describe('Doc Routing', () => {
await responsePromise;
await expect(page.getByText('Log in to access the document.')).toBeVisible({
timeout: 10000,
});
await expect(page.locator('meta[name="robots"]')).toHaveAttribute(
'content',
'noindex',
);
await expect(page).toHaveTitle(/401 Unauthorized - Docs/);
await expect(page.getByText('Log in to access the document')).toBeVisible();
});
});

View File

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

View File

@@ -8,6 +8,8 @@ test.beforeEach(async ({ page }) => {
test.describe('Doc Table Content', () => {
test('it checks the doc table content', async ({ page, browserName }) => {
test.setTimeout(60000);
const [randomDoc] = await createDoc(
page,
'doc-table-content',

View File

@@ -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();
});
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable playwright/no-conditional-in-test */
import { expect, test } from '@playwright/test';
import {
@@ -50,7 +51,7 @@ test.describe('Doc Tree', () => {
await expect(subPageItem).toBeVisible();
await subPageItem.click();
await verifyDocName(page, '');
const input = page.getByRole('textbox', { name: 'Document title' });
const input = page.getByRole('textbox', { name: 'doc title input' });
await input.click();
const [randomDocName] = randomName('doc-tree-test', browserName, 1);
await input.fill(randomDocName);
@@ -196,7 +197,7 @@ test.describe('Doc Tree', () => {
await page.getByText('Move to my docs').click();
await expect(
page.getByRole('textbox', { name: 'Document title' }),
page.getByRole('textbox', { name: 'doc title input' }),
).not.toHaveText(docChild);
const header = page.locator('header').first();
@@ -220,11 +221,11 @@ test.describe('Doc Tree', () => {
const list = page.getByTestId('doc-share-quick-search');
const currentUser = list.getByTestId(
`doc-share-member-row-user.test@${browserName}.test`,
`doc-share-member-row-user@${browserName}.test`,
);
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await currentUserRole.click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByLabel('Administrator').click();
await list.click();
await page.getByRole('button', { name: 'Ok' }).click();
@@ -235,12 +236,6 @@ test.describe('Doc Tree', () => {
'doc-tree-detach-child',
);
await expect(
page
.getByLabel('It is the card information about the document.')
.getByText('Administrator ·'),
).toBeVisible();
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(docChild)).toBeVisible();
await docTree.click();
@@ -258,56 +253,12 @@ test.describe('Doc Tree', () => {
page.getByRole('menuitem', { name: 'Move to my docs' }),
).toHaveAttribute('aria-disabled', 'true');
});
test('keyboard navigation with Enter key opens documents', async ({
page,
browserName,
}) => {
// Create a parent document
const [docParent] = await createDoc(
page,
'doc-tree-keyboard-nav',
browserName,
1,
);
await verifyDocName(page, docParent);
// Create a sub-document
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-tree-keyboard-child',
);
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible();
// Test keyboard navigation on root document
const rootItem = page.getByTestId('doc-tree-root-item');
await expect(rootItem).toBeVisible();
// Focus on the root item and press Enter
await rootItem.focus();
await expect(rootItem).toBeFocused();
await page.keyboard.press('Enter');
// Verify we navigated to the root document
await verifyDocName(page, docParent);
await expect(page).toHaveURL(/\/docs\/[^/]+\/?$/);
// Now test keyboard navigation on sub-document
await expect(docTree.getByText(docChild)).toBeVisible();
});
});
test.describe('Doc Tree: Inheritance', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('A child inherit from the parent', async ({ page, browserName }) => {
// test.slow() to extend timeout since this scenario chains Keycloak login + redirects,
// doc creation/navigation and async doc-tree loading (/documents/:id/tree), which can exceed 30s (especially in CI).
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -320,7 +271,7 @@ test.describe('Doc Tree: Inheritance', () => {
await verifyDocName(page, docParent);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
@@ -356,7 +307,6 @@ test.describe('Doc Tree: Inheritance', () => {
await expect(page.locator('h2').getByText(docChild)).toBeVisible();
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible({ timeout: 10000 });
await expect(docTree.getByText(docParent)).toBeVisible();
});
});

View File

@@ -18,7 +18,7 @@ test.describe('Doc Version', () => {
await verifyDocName(page, randomDoc);
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await page.getByLabel('Version history').click();
await expect(page.getByText('History', { exact: true })).toBeVisible();
const modal = page.getByLabel('version history modal');
@@ -54,7 +54,7 @@ test.describe('Doc Version', () => {
).toBeVisible();
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await page.getByLabel('Version history').click();
await expect(panel).toBeVisible();
await expect(page.getByText('History', { exact: true })).toBeVisible();
@@ -82,9 +82,7 @@ test.describe('Doc Version', () => {
await verifyDocName(page, 'Mocked document');
await page.getByLabel('Open the document options').click();
await expect(
page.getByRole('menuitem', { name: 'Version history' }),
).toBeDisabled();
await expect(page.getByLabel('Version history')).toBeDisabled();
});
test('it restores the doc version', async ({ page, browserName }) => {
@@ -111,7 +109,7 @@ test.describe('Doc Version', () => {
await expect(page.getByText('World')).toBeVisible();
await page.getByLabel('Open the document options').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await page.getByLabel('Version history').click();
const modal = page.getByLabel('version history modal');
const panel = modal.getByLabel('version list');

View File

@@ -7,7 +7,6 @@ import {
keyCloakSignIn,
verifyDocName,
} from './utils-common';
import { addNewMember, connectOtherUserToDoc } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc Visibility', () => {
@@ -16,6 +15,7 @@ test.describe('Doc Visibility', () => {
});
test('It checks the copy link button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
@@ -41,27 +41,23 @@ test.describe('Doc Visibility', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await expect(selectVisibility.getByText('Private')).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Read only' }),
).toBeHidden();
await expect(
page.getByRole('menuitem', { name: 'Can read and edit' }),
).toBeHidden();
await expect(page.getByLabel('Read only')).toBeHidden();
await expect(page.getByLabel('Can read and edit')).toBeHidden();
await selectVisibility.click();
await page.getByRole('menuitem', { name: 'Connected' }).click();
await page.getByLabel('Connected').click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
await selectVisibility.click();
await page.getByRole('menuitem', { name: 'Public' }).click();
await page.getByLabel('Public', { exact: true }).click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
});
});
@@ -123,11 +119,8 @@ test.describe('Doc Visibility: Restricted', () => {
.click();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
await keyCloakSignIn(page, otherBrowser);
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
@@ -151,31 +144,44 @@ test.describe('Doc Visibility: Restricted', () => {
await verifyDocName(page, docTitle);
await page
.locator('.ProseMirror')
.locator('.bn-block-outer')
.last()
.fill('Hello World');
const docUrl = page.url();
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
browserName,
docUrl,
});
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),
).toBeVisible({
timeout: 10000,
});
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Reader', otherBrowserName);
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
await otherPage.reload();
await expect(otherPage.getByText('Hello World')).toBeVisible();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
const username = `user@${otherBrowser}.test`;
await inputSearch.fill(username);
await page.getByRole('option', { name: username }).click();
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByLabel('doc-role-dropdown').click();
await page.getByLabel('Reader').click();
await page.getByRole('button', { name: 'Invite' }).click();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
const urlDoc = page.url();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
await expect(page.getByLabel('Share button')).toBeVisible();
});
});
@@ -199,7 +205,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
@@ -212,8 +218,8 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
await page.getByLabel('Visibility mode').click();
await page
.getByRole('menuitem', {
name: 'Reading',
@@ -283,7 +289,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
@@ -296,8 +302,8 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await page.getByLabel('Visibility mode').click();
await page.getByLabel('Editing').click();
await expect(
page.getByText('The document visibility has been updated.').first(),
@@ -352,7 +358,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -404,7 +410,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -443,10 +449,7 @@ test.describe('Doc Visibility: Authenticated', () => {
.click();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
await keyCloakSignIn(page, otherBrowser);
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
@@ -492,7 +495,6 @@ test.describe('Doc Visibility: Authenticated', () => {
page,
browserName,
}) => {
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -506,7 +508,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -519,8 +521,8 @@ test.describe('Doc Visibility: Authenticated', () => {
).toBeVisible();
const urlDoc = page.url();
await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await page.getByLabel('Visibility mode').click();
await page.getByLabel('Editing').click();
await expect(
page.getByText('The document visibility has been updated.').first(),
@@ -535,22 +537,15 @@ test.describe('Doc Visibility: Authenticated', () => {
.click();
const otherBrowser = BROWSERS.find((b) => b !== browserName);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
await keyCloakSignIn(page, otherBrowser);
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible({
timeout: 10000,
});
await expect(page.getByText('Link Copied !')).toBeVisible();
});
});

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