Compare commits

..

1 Commits

Author SHA1 Message Date
Manuel Raynaud
17e28c487c 📝(dev) add cursor rule for django code
We apply a cursor rule to the project related to the django application.
This rule is heavily inspired from the posthog's rule.
2025-03-31 10:44:14 +02:00
316 changed files with 19847 additions and 28634 deletions

View File

@@ -0,0 +1,78 @@
---
description: Rules for writing Python with Django
globs: src/backend/**/*.py
alwaysApply: false
---
You are an expert in Python, Django, and scalable web application development.
Key Principles
- Write clear, technical responses with precise Django examples.
- Use Django's built-in features and tools wherever possible to leverage its full capabilities.
- Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance for the most part, with the one exception being 100 characters per line instead of 79).
- Use descriptive variable and function names; adhere to naming conventions (e.g., lowercase with underscores for functions and variables).
Django/Python
- Use Django REST Framework viewsets for API endpoints.
- Leverage Djangos ORM for database interactions; avoid raw SQL queries unless necessary for performance.
- Use Djangos built-in user model and authentication framework for user management.
- Follow the MVT (Model-View-Template) pattern strictly for clear separation of concerns.
- Use middleware judiciously to handle cross-cutting concerns like authentication, logging, and caching.
Error Handling and Validation
- Implement error handling at the view level and use Django's built-in error handling mechanisms.
- Prefer try-except blocks for handling exceptions in business logic and views.
Dependencies
- Django
- Django REST Framework (for API development)
- Celery (for background tasks)
- Redis (for caching and task queues)
- PostgreSQL (preferred databases for production)
- Minio (file storage for production)
- OIDC prodiver (for managing authentication)
Django-Specific Guidelines
- Use Django templates for rendering HTML and DRF serializers for JSON responses.
- Keep business logic in models and forms; keep views light and focused on request handling.
- Use Django's URL dispatcher (urls.py) to define clear and RESTful URL patterns.
- Apply Django's security best practices (e.g., CSRF protection, SQL injection protection, XSS prevention).
- Use Djangos built-in tools for testing (pytest-django) to ensure code quality and reliability.
- Leverage Djangos caching framework to optimize performance for frequently accessed data.
- Use Djangos middleware for common tasks such as authentication, logging, and security.
Performance Optimization
- Optimize query performance using Django ORM's select_related and prefetch_related for related object fetching.
- Use Djangos cache framework with backend support (e.g., Redis or Memcached) to reduce database load.
- Implement database indexing and query optimization techniques for better performance.
- Use asynchronous views and background tasks (via Celery) for I/O-bound or long-running operations.
- Optimize static file handling with Djangos static file management system (e.g., WhiteNoise).
Logging
- As a general rule, we should have logs for every expected and unexpected actions of the application, using the appropriate log level.
- We should also be logging these exceptions to Sentry with the Sentry Python SDK. Python exceptions should almost always be captured automatically without extra instrumentation, but custom ones (such as failed requests to external services, query errors, or Celery task failures) can be tracked using capture_exception().
Log Levels
- A log level or log severity is a piece of information telling how important a given log message is:
- DEBUG: should be used for information that may be needed for diagnosing issues and troubleshooting or when running application in the test environment for the purpose of making sure everything is running correctly
- INFO: should be used as standard log level, indicating that something happened
- WARN: should be used when something unexpected happened but the code can continue the work
- ERROR: should be used when the application hits an issue preventing one or more functionalities from properly functioning
Security
- Dont log sensitive information. Make sure you never log:
- authorization tokens
- passwords
- financial data
- health data
- PII (Personal Identifiable Information)
Testing
- All new packages and most new significant functionality should come with unit tests
Unit tests
- A good unit test should:
- focus on a single use-case at a time
- have a minimal set of assertions per test
- demonstrate every use case. The rule of thumb is: if it can happen, it should be covered
Refer to Django documentation for best practices in views, models, forms, and security considerations.

View File

@@ -1,7 +1,7 @@
---
name: 🐛 Bug Report
about: If something is not working as expected 🤔.
labels: ["bug", "triage"]
---
## Bug Report
@@ -18,8 +18,8 @@ A clear and concise description of what you expected to happen (or code).
3. And then the bug happens!
**Environment**
- Docs version:
- Instance url:
- Impress version:
- Platform:
**Possible Solution**
<!--- Only if you have suggestions on a fix for the bug -->

View File

@@ -1,7 +1,7 @@
---
name: ✨ Feature Request
about: I have a suggestion (and may want to build it 💪)!
labels: ["feature", "triage"]
---
## Feature Request
@@ -16,8 +16,8 @@ A clear and concise description of what you want to happen. Add any considered d
A clear and concise description of any alternative solutions or features you've considered.
**Discovery, Documentation, Adoption, Migration Strategy**
If you can, explain how users will be able to use this and possibly write out some documentation (if applicable).
Maybe add a screenshot or design?
If you can, explain how users will be able to use this and possibly write out a version the docs (if applicable).
Maybe a screenshot or design?
**Do you want to work on it through a Pull Request?**
<!-- Make sure to coordinate with us before you spend too much time working on an implementation! -->

View File

@@ -1,13 +1,17 @@
---
name: 🤗 Support Question
about: If you have a question 💬, or something was not clear from the docs!
labels: ["support", "triage"]
---
## Support request
**Checks before filing**
Please make sure you have read our [main Readme](https://github.com/suitenumerique/docs).
Also make sure it was not already answered in [an open or close issue](https://github.com/suitenumerique/docs/issues?q=is%3Aissue%20state%3Aopen%20label%3Asupport).
---
<!-- ^ Click "Preview" for a nicer view! ^
We primarily use GitHub as an issue tracker. If however you're encountering an issue not covered in the docs, we may be able to help! -->
---
Please make sure you have read our [main Readme](https://github.com/numerique-gouv/impress).
Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/impress/issues).
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌

View File

@@ -25,7 +25,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.13.3"
python-version: "3.12.6"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies

View File

@@ -11,7 +11,6 @@ on:
pull_request:
branches:
- 'main'
- 'ci/trivy-fails'
env:
DOCKER_USER: 1001:127
@@ -39,6 +38,7 @@ jobs:
with:
docker-build-args: '--target backend-production -f Dockerfile'
docker-image-name: 'docker.io/lasuite/impress-backend:${{ github.sha }}'
continue-on-error: true
-
name: Build and push
uses: docker/build-push-action@v6
@@ -72,6 +72,7 @@ jobs:
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
continue-on-error: true
-
name: Build and push
uses: docker/build-push-action@v6
@@ -79,9 +80,7 @@ jobs:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
PUBLISH_AS_MIT=false
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -107,7 +106,8 @@ jobs:
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f src/frontend/servers/y-provider/Dockerfile --target y-provider'
docker-image-name: 'docker.io/lasuite/impress-y-provider:${{ github.sha }}'
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
continue-on-error: true
-
name: Build and push
uses: docker/build-push-action@v6

View File

@@ -61,25 +61,6 @@ jobs:
exit 1
fi
lint-spell-mistakes:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install codespell
run: pip install --user codespell
- name: Check for typos
run: |
codespell \
--check-filenames \
--ignore-words-list "Dokument,afterAll,excpt,statics" \
--skip "./git/" \
--skip "**/*.po" \
--skip "**/*.pot" \
--skip "**/*.json" \
--skip "**/yarn.lock"
lint-back:
runs-on: ubuntu-latest
defaults:
@@ -91,7 +72,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.13.3"
python-version: "3.12.6"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
@@ -186,7 +167,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.13.3"
python-version: "3.12.6"
- name: Install development dependencies
run: pip install --user .[dev]

View File

@@ -8,90 +8,9 @@ and this project adheres to
## [Unreleased]
## [3.3.0] - 2025-05-06
### Added
- ✨(backend) add endpoint checking media status #984
- ✨(backend) allow setting session cookie age via env var #977
- ✨(backend) allow theme customnization using a configuration file #948
- ✨(frontend) Add a custom callout block to the editor #892
- 🚩(frontend) version MIT only #911
- ✨(backend) integrate maleware_detection from django-lasuite #936
- 🏗️(frontend) Footer configurable #959
- 🩺(CI) add lint spell mistakes #954
- ✨(frontend) create generic theme #792
- 🛂(frontend) block edition to not connected users #945
- 🚸(frontend) Let loader during upload analyze #984
- 🚩(frontend) feature flag on blocking edition #997
### Changed
- 📝(frontend) Update documentation #949
- ✅(frontend) Improve tests coverage #949
- ⬆️(docker) upgrade backend image to python 3.13 #973
- ⬆️(docker) upgrade node images to alpine 3.21 #973
### Fixed
- 🐛(y-provider) increase JSON size limits for transcription conversion #989
### Removed
- 🔥(back) remove footer endpoint #948
## [3.2.1] - 2025-05-06
## Fixed
- 🐛(frontend) fix list copy paste #943
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
## [3.2.0] - 2025-05-05
## Added
- 🚸(backend) make document search on title accent-insensitive #874
- 🚩 add homepage feature flag #861
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
- ✨(settings) Allow configuring PKCE for the SSO #886
- 🌐(i18n) activate chinese and spanish languages #884
- 🔧(backend) allow overwriting the data directory #893
- (backend) add `django-lasuite` dependency #839
- ✨(frontend) advanced table features #908
## Changed
- ⚡️(frontend) reduce unblocking time for config #867
- ♻️(frontend) bind UI with ability access #900
- ♻️(frontend) use built-in Quote block #908
## Fixed
- 🐛(nginx) fix 404 when accessing a doc #866
- 🔒️(drf) disable browsable HTML API renderer #919
- 🔒(frontend) enhance file download security #889
- 🐛(backend) race condition create doc #633
- 🐛(frontend) fix breaklines in custom blocks #908
## [3.1.0] - 2025-04-07
## Added
- 🚩(backend) add feature flag for the footer #841
- 🔧(backend) add view to manage footer json #841
- ✨(frontend) add custom css style #771
- 🚩(frontend) conditionally render AI button only when feature is enabled #814
## Changed
- 🚨(frontend) block button when creating doc #749
## Fixed
- 🐛(back) validate document content in serializer #822
- 🐛(frontend) fix selection click past end of content #840
## [3.0.0] - 2025-03-28
@@ -202,6 +121,7 @@ and this project adheres to
- ♻️(frontend) improve table pdf rendering
- 🐛(email) invitation emails in receivers language
## [2.2.0] - 2025-02-10
## Added
@@ -572,7 +492,7 @@ and this project adheres to
- ⚡️(e2e) unique login between tests (#80)
- ⚡️(CI) improve e2e job (#86)
- ♻️(frontend) improve the error and message info ui (#93)
- ✏️(frontend) change all occurrences of pad to doc (#99)
- ✏️(frontend) change all occurences of pad to doc (#99)
## Fixed
@@ -590,11 +510,7 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.3.0...main
[v3.3.0]: https://github.com/numerique-gouv/impress/releases/v3.3.0
[v3.2.1]: https://github.com/numerique-gouv/impress/releases/v3.2.1
[v3.2.0]: https://github.com/numerique-gouv/impress/releases/v3.2.0
[v3.1.0]: https://github.com/numerique-gouv/impress/releases/v3.1.0
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.0.0...main
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
[v2.6.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0

View File

@@ -42,38 +42,34 @@ Examples of unacceptable behavior include:
## Enforcement Guidelines
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of the following Code of Conduct
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this
## Code of Conduct:
### 1. Correction
1. Correction
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
3. Temporary Ban
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
## Attribution
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozilla's [code of conduct enforcement ladder](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md).
Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder.
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

View File

@@ -2,16 +2,16 @@
Thank you for taking the time to contribute! Please follow these guidelines to ensure a smooth and productive workflow. 🚀🚀🚀
To get started with the project, please refer to the [README.md](https://github.com/suitenumerique/docs/blob/main/README.md) for detailed instructions on how to run Docs locally.
To get started with the project, please refer to the [README.md](https://github.com/suitenumerique/docs/blob/main/README.md) for detailed instructions.
Contributors are required to sign off their commits with `git commit --signoff`: this confirms that they have read and accepted the [Developer's Certificate of Origin 1.1](https://developercertificate.org/). For security reasons we also require [signing your commits with your SSH or GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) with `git commit -S`.
Contributors are required to sign off their commits with `git commit --sign-off`: this confirms that they have read and accepted the [Developer's Certificate of Origin 1.1](https://developercertificate.org/).
Please also check out our [dev handbook](https://suitenumerique.gitbook.io/handbook) to learn our best practices.
## Help us with translations
You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
Your language is not there? Request it on our Crowdin page 😊 or ping us on [Matrix](https://matrix.to/#/#docs-official:matrix.org) and let us know if you can help with translations and/or proofreading.
Your language is not there? Request it on our Crowdin page 😊.
## Creating an Issue
@@ -35,14 +35,10 @@ All commit messages must adhere to the following format:
`<gitmoji>(type) title description`
* <**gitmoji**>: Use a gitmoji to represent the purpose of the commit. For example, ✨ for adding a new feature or 🔥 for removing something, see the list [here](https://gitmoji.dev/).
* <**gitmoji**>: Use a gitmoji to represent the purpose of the commit. For example, ✨ for adding a new feature or 🔥 for removing something, see the list here: <https://gitmoji.dev/>.
* **(type)**: Describe the type of change. Common types include `backend`, `frontend`, `CI`, `docker` etc...
* **title**: A short, descriptive title for the change (*)
* **blank line after the commit title
* **description**: Include additional details on why you made the changes (**).
(*) ⚠️ **Make sure you add no space between the emoji and the (type) but add a space after the closing parenthesis of the type and use no caps!**
(**) ⚠️ **Commit description message is mandatory and shouldn't be too long**
* **title**: A short, descriptive title for the change.
* **description**: Include additional details about what was changed and why.
### Example Commit Message
@@ -70,9 +66,7 @@ Please add a line to the changelog describing your development. The changelog en
It is nice to add information about the purpose of the pull request to help reviewers understand the context and intent of the changes. If you can, add some pictures or a small video to show the changes.
### Don't forget to:
- signoff your commits
- sign your commits with your key (SSH, GPG etc.)
- check your commits (see warnings above)
- check your commits
- check the linting: `make lint && make frontend-lint`
- check the tests: `make test`
- add a changelog entry
@@ -92,11 +86,3 @@ Make sure that all new features or fixes have corresponding tests. Run the test
If you need any help while contributing, feel free to open a discussion or ask for guidance in the issue tracker. We are more than happy to assist!
Thank you for your contributions! 👍
## Contribute to BlockNote
We use [BlockNote](https://www.blocknotejs.org/) for the text editing features of Docs.
If you find and issue with the editor you can [report it](https://github.com/TypeCellOS/BlockNote/issues) directly on their repository.
Please consider contributing to BlockNotejs, as a library, it's useful to many projects not just Docs.
The project is licended with Mozilla Public License Version 2.0 but be aware that [XL packages](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE) are dual licenced with GNU AFFERO GENERAL PUBLIC LICENCE Version 3 and proprietary licence if you are [sponsor](https://www.blocknotejs.org/pricing).

View File

@@ -1,7 +1,7 @@
# Django impress
# ---- base image to inherit from ----
FROM python:3.13.3-alpine AS base
FROM python:3.12.6-alpine3.20 AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip setuptools
@@ -30,7 +30,7 @@ RUN mkdir /install && \
# ---- mails ----
FROM node:24 AS mail-builder
FROM node:20 AS mail-builder
COPY ./src/mail /mail/app
@@ -139,9 +139,6 @@ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# ---- Production image ----
FROM core AS backend-production
# Remove apk cache, we don't need it anymore
RUN rm -rf /var/cache/apk/*
ARG IMPRESS_STATIC_ROOT=/data/static
# Gunicorn

View File

@@ -1,19 +1,13 @@
<p align="center">
<a href="https://github.com/suitenumerique/docs">
<img alt="Docs" src="/docs/assets/banner-docs.png" width="100%" />
<img alt="Docs" src="/docs/assets/docs-logo.png" width="300" />
</a>
</p>
<p align="center">
<a href="https://github.com/suitenumerique/docs/stargazers/">
<img src="https://img.shields.io/github/stars/suitenumerique/docs" alt="">
</a>
<a href='https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md'><img alt='PRs Welcome' src='https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shields'/></a>
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/suitenumerique/docs"/>
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/suitenumerique/docs"/>
<a href="https://github.com/suitenumerique/docs/blob/main/LICENSE">
<img alt="GitHub closed issues" src="https://img.shields.io/github/license/suitenumerique/docs"/>
</a>
Welcome to Docs! The open source document editor where your notes can become knowledge through live collaboration
</p>
<p align="center">
<a href="https://matrix.to/#/#docs-official:matrix.org">
Chat on Matrix
@@ -26,52 +20,48 @@
</a>
</p>
# La Suite Docs : Collaborative Text Editing
Docs, where your notes can become knowledge through live collaboration.
<img src="/docs/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
## Why use Docs ❓
⚠️ **Note that Docs provides docs/pdf exporters by loading [two BlockNote packages](https://github.com/suitenumerique/docs/blob/main/src/frontend/apps/impress/package.json#L22C7-L23C53), which we use under the AGPL-3.0 licence. Until we comply with the terms of this license, we recommend that you don't run Docs as a commercial product, unless you are willing to sponsor [BlockNote](https://github.com/TypeCellOS/BlockNote).**
Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing.
It offers a scalable and secure alternative to tools such as Google Docs, Notion (without the dbs), Outline, or Confluence.
### Write
* 😌 Get simple, accessible online editing for your team.
* 💅 Create clean documents with beautiful formatting options.
* 🖌️ Focus on your content using either the in-line editor, or [the Markdown syntax](https://www.markdownguide.org/basic-syntax/).
* 🧱 Quickly design your page thanks to the many block types, accessible from the `/` slash commands, as well as keyboard shortcuts.
* 🔌 Write offline! Your edits will be synced once you're back online.
* ✨ Save time thanks to our AI actions, such as rephrasing, summarizing, fixing typos, translating, etc. You can even turn your selected text into a prompt!
* 😌 Simple collaborative editing without the formatting complexity of markdown
* 🔌 Offline? No problem, keep writing, your edits will get synced when back online
* 💅 Create clean documents with limited but beautiful formatting options and focus on content
* 🧱 Built for productivity (markdown support, many block types, slash commands, keyboard shortcuts).
* ✨ Save time thanks to our AI actions (generate, sum up, correct, translate)
### Work together
* 🤝 Enjoy live editing! See your team collaborate in real time.
* 🔒 Keep your information secure thanks to granular access control. Only share with the right people.
* 📑 Export your content in multiple formats (`.odt`, `.docx`, `.pdf`) with customizable templates.
* 📚 Turn your team's collaborative work into organized knowledge with Subpages.
### Collaborate
* 🤝 Collaborate with your team in real time
* 🔒 Granular access control to ensure your information is secure and only shared with the right people
* 📑 Professional document exports in multiple formats (.odt, .doc, .pdf) with customizable templates
* 📚 Built-in wiki functionality to turn your team's collaborative work into organized knowledge `ETA 02/2025`
### Self-host
🚀 Docs is easy to install on your own servers
Available methods: Helm chart, Nix package
In the works: Docker Compose, YunoHost
⚠️ 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.
* 🚀 Easy to install, scalable and secure alternative to Notion, Outline or Confluence
## Getting started 🔧
### Test it
You can test Docs on your browser by visiting this [demo document](https://impress-preprod.beta.numerique.gouv.fr/docs/6ee5aac4-4fb9-457d-95bf-bb56c2467713/)
Test Docs on your browser by logging in on this [environment](https://impress-preprod.beta.numerique.gouv.fr/)
### Run Docs locally
```
email: test.docs@yopmail.com
password: I'd<3ToTestDocs
```
> ⚠️ The methods described below for running Docs locally is **for testing purposes only**. It is based on building Docs using [Minio](https://min.io/) as an S3-compatible storage solution. Of course you can choose any S3-compatible storage solution.
### Run it locally
> ⚠️ Running Docs locally using the methods described below is for testing purposes only. It is based on building Docs using Minio as the S3 storage solution but you can choose any S3 compatible object storage of your choice.
**Prerequisite**
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop, then type:
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop:
```shellscript
$ docker -v
@@ -83,7 +73,7 @@ $ docker compose version
Docker Compose version v2.32.4
```
> ⚠️ You may need to run the following commands with `sudo`, but this can be avoided by adding your user to the local `docker` group.
> ⚠️ You may need to run the following commands with sudo but this can be avoided by adding your user to the `docker` group.
**Project bootstrap**
@@ -93,13 +83,13 @@ The easiest way to start working on the project is to use [GNU Make](https://www
$ make bootstrap FLUSH_ARGS='--no-input'
```
This command builds the `app` container, installs dependencies, performs database migrations and compiles translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues.
This command builds the `app` container, installs dependencies, performs database migrations and compile translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues.
Your Docker services should now be up and running 🎉
You can access to the project by going to <http://localhost:3000>.
You will be prompted to log in. The default credentials are:
You will be prompted to log in, the default credentials are:
```
username: impress
@@ -133,14 +123,13 @@ $ make run-backend
```
**Adding content**
You can create a basic demo site by running this command:
You can create a basic demo site by running:
```shellscript
$ make demo
```
Finally, you can check all available Make rules using this command:
Finally, you can check all available Make rules using:
```shellscript
$ make help
@@ -148,7 +137,7 @@ $ make help
**Django admin**
You can access the Django admin site at:
You can access the Django admin site at
<http://localhost:8071/admin>.
@@ -160,7 +149,7 @@ $ make superuser
## Feedback 🙋‍♂️🙋‍♀️
We'd love to hear your thoughts, and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#docs-official:matrix.org).
We'd love to hear your thoughts and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#docs-official:matrix.org).
## Roadmap
@@ -170,7 +159,7 @@ Want to know where the project is headed? [🗺️ Checkout our roadmap](https:/
This work is released under the MIT License (see [LICENSE](https://github.com/suitenumerique/docs/blob/main/LICENSE)).
While Docs is a public-driven initiative, our licence choice is an invitation for private sector actors to use, sell and contribute to the project.
While Docs is a public driven initiative our licence choice is an invitation for private sector actors to use, sell and contribute to the project.
## Contributing 🙌
@@ -178,9 +167,9 @@ This project is intended to be community-driven, so please, do not hesitate to [
You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
If you intend to make pull requests, see [CONTRIBUTING](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md) for guidelines.
If you intend to make pull requests see [CONTRIBUTING](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md) for guidelines.
## Directory structure:
Directory structure:
```markdown
docs
@@ -198,14 +187,14 @@ docs
### Stack
Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/). We thank the contributors of all these projects for their awesome work!
We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/).
Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/).
### Gov ❤️ open source
Docs is the result of a joint effort led by the French 🇫🇷🥖 ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German 🇩🇪🥨 governments ([ZenDiS](https://zendis.de/)).
We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/).
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱🧀), feel free to [reach out](mailto:docs@numerique.gouv.fr) if you are interested in using or contributing to Docs.
<p align="center">

View File

@@ -4,7 +4,7 @@
Security is very important to us.
If you have any issue regarding security, please disclose the information responsibly submitting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at docs@numerique.gouv.fr
If you have any issue regarding security, please disclose the information responsibly submiting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at docs@numerique.gouv.fr
We appreciate your effort to make Docs more secure.

View File

@@ -16,15 +16,6 @@ the following command inside your docker container:
## [Unreleased]
## [3.3.0] - 2025-05-22
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/docs/env.md) for more information.
The footer is now configurable from a customization file. To override the default one, you can
use the `THEME_CUSTOMIZATION_FILE_PATH` environment variable to point to your customization file.
The customization file must be a JSON file and must follow the rules described in the
[theming documentation](docs/theming.md).
## [3.0.0] - 2025-03-28
We are not using the nginx auth request anymore to access the collaboration server (`yProvider`)

View File

@@ -155,7 +155,8 @@ services:
target: frontend-production
args:
API_ORIGIN: "http://localhost:8071"
PUBLISH_AS_MIT: "false"
Y_PROVIDER_URL: "ws://localhost:4444"
MEDIA_URL: "http://localhost:8083"
SW_DEACTIVATED: "true"
image: impress:frontend-development
ports:
@@ -184,11 +185,15 @@ services:
context: .
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
target: y-provider
command: ["yarn", "workspace", "server-y-provider", "run", "dev"]
working_dir: /app/frontend
restart: unless-stopped
env_file:
- env.d/development/common
ports:
- "4444:4444"
volumes:
- ./src/frontend/:/app/frontend
kc_postgresql:
image: postgres:14.3

View File

@@ -1,193 +0,0 @@
## Decision TLDR;
We will use Yjs a CRDT-based library for the collaborative editing of the documents.
## Status
Accepted
## Context
We need to implement a collaborative editing feature for the documents that supports real-time collaboration, offline capabilities, and seamless integration with our Django backend.
## Considered alternatives
### ProseMirror
A robust toolkit for building rich-text editors with collaboration capabilities.
| Pros | Cons |
| --- | --- |
| Mature ecosystem | Complex integration with Django |
| Rich text editing features | Steeper learning curve |
| Used by major companies | More complex to implement offline support |
| Large community | |
### ShareDB
Real-time database backend based on Operational Transformation.
| Pros | Cons |
| --- | --- |
| Battle-tested in production | Complex setup required |
| Strong consistency model | Requires specific backend architecture |
| Good documentation | Less flexible with different backends |
| | Higher latency compared to CRDTs |
### Convergence
Complete enterprise solution for real-time collaboration.
| Pros | Cons |
| --- | --- |
| Full-featured solution | Commercial licensing |
| Built-in presence features | Less community support |
| Enterprise support | More expensive |
| Good offline support | Overkill for basic needs |
### CRDT-based Solutions Comparison
A CRDT-based library specifically designed for real-time collaboration.
| Category | Pros | Cons |
|----------|------|------|
| Technical Implementation | • Native real-time collaboration<br>• No central conflict resolution needed<br>• Works well with Django backend<br>• Automatic state synchronization | • Learning curve for CRDT concepts<br>• More complex initial setup<br>• Additional metadata overhead |
| User Experience | • Instant local updates<br>• Works offline by default<br>• Low latency<br>• Smooth concurrent editing | • Eventual consistency might cause brief inconsistencies<br>• UI must handle temporary conflicts |
| Performance | • Excellent scaling with multiple users<br>• Reduced server load<br>• Efficient network usage<br>• Good memory optimization (especially Yjs) | • Slightly higher memory usage<br>• Initial state sync can be larger |
| Development | • No need to build conflict resolution<br>• Simple integration with text editors<br>• Future-proof architecture | • Team needs to learn new concepts<br>• Fewer ready-made solutions<br>• May need to build some features from scratch |
| Maintenance | • Less server infrastructure<br>• Simpler deployment<br>• Fewer points of failure | • Debugging can be more complex<br>• State management requires careful handling |
| Business Impact | • Better offline support for users<br>• Scales well as user base grows<br>• No licensing costs (with Yjs) | • Initial development time might be longer<br>• Team training required |
#### Yjs
- **Type**: State-based CRDT
- **Implementation**: JavaScript/TypeScript
- **Features**:
- Rich text collaboration
- Shared types (Array, Map, XML)
- Binary encoding
- P2P support
- **Performance**: Excellent for text editing
- **Memory Usage**: Optimized
- **License**: MIT
#### Automerge
- **Type**: Operation-based CRDT
- **Implementation**: JavaScript/Rust
- **Features**:
- JSON-like data structures
- Change history
- Undo/Redo
- Binary format
- **Performance**: Good, with Rust backend
- **Memory Usage**: Higher than Yjs
- **License**: MIT
#### Legion
- **Type**: State-based CRDT
- **Implementation**: Rust with JS bindings
- **Features**:
- High performance
- Memory efficient
- Binary protocol
- **Performance**: Excellent
- **Memory Usage**: Very efficient
- **License**: Apache 2.0
#### Diamond Types
- **Type**: Operation-based CRDT
- **Implementation**: TypeScript
- **Features**:
- Specialized for text
- Small memory footprint
- Simple API
- **Performance**: Good for text
- **Memory Usage**: Efficient
- **License**: MIT
Comparison Table:
| Feature | Yjs | Automerge | Legion | Diamond Types |
|---------|-----|-----------|--------|---------------|
| Text Editing | ✅ Excellent | ✅ Good | ⚠️ Basic | ✅ Excellent |
| Structured Data | ✅ | ✅ | ✅ | ⚠️ |
| Memory Efficiency | ✅ High | ⚠️ Medium | ✅ Very High | ✅ High |
| Network Efficiency | ✅ | ⚠️ | ✅ | ✅ |
| Maturity | ✅ | ✅ | ⚠️ | ⚠️ |
| Community Size | ✅ Large | ✅ Large | ⚠️ Small | ⚠️ Small |
| Documentation | ✅ | ✅ | ⚠️ | ⚠️ |
| Backend Options | ✅ Many | ✅ Many | ⚠️ Limited | ⚠️ Limited |
Key Differences:
1. **Implementation Approach**:
- Yjs: Optimized for text and rich-text editing
- Automerge: General-purpose JSON CRDT
- Legion: Performance-focused with Rust
- Diamond Types: Specialized for text collaboration
2. **Performance Characteristics**:
- Yjs: Best for text editing scenarios
- Automerge: Good all-around performance
- Legion: Excellent raw performance
- Diamond Types: Optimized for text
3. **Ecosystem Integration**:
- Yjs: Wide range of integrations
- Automerge: Good JavaScript ecosystem
- Legion: Limited but growing
- Diamond Types: Focused on text editors
This analysis reinforces our choice of Yjs for the CRDT-based option as it provides:
- Best-in-class text editing performance
- Mature ecosystem
- Active community
- Excellent documentation
- Wide range of backend options
## Decision
After evaluating the alternatives, we choose Yjs for the following reasons:
1. **Technical Fit:**
- Native CRDT support ensures reliable collaboration
- Excellent offline capabilities
- Good performance characteristics
- Flexible backend integration options
2. **Project Requirements Match:**
- Easy integration with our Django backend
- Supports our core collaborative features
- Manageable learning curve for the team
3. **Community & Support:**
- Active development
- Growing community
- Good documentation
- Open source with MIT license
### Comparison of Key Features:
| Feature | Yjs (CRDT) | ProseMirror | ShareDB | Convergence |
|---------|-----|-------------|----------|-------------|
| Real-time Collaboration | ✅ | ✅ | ✅ | ✅ |
| Offline Support | ✅ | ⚠️ | ⚠️ | ✅ |
| Django Integration | Easy | Complex | Complex | Moderate |
| Learning Curve | Medium | High | High | Medium |
| Cost | Free | Free | Free | Paid |
| Community Size | Growing | Large | Medium | Small |
## Consequences
### Positive
- Simplified implementation of real-time collaboration
- Good developer experience
- Future-proof technology choice
- No licensing costs
### Negative
- Team needs to learn CRDT concepts
- Newer technology compared to alternatives
- May need to build some features available out-of-the-box in other solutions
### Risks
- Community support might not grow as expected
- May discover limitations as we scale

View File

@@ -1,19 +0,0 @@
## Architecture
### Global system architecture
```mermaid
flowchart TD
User -- HTTP --> Front("Frontend (NextJS SPA)")
Front -- REST API --> Back("Backend (Django)")
Front -- WebSocket --> Yserver("Microservice Yjs (Express)") -- WebSocket --> CollaborationServer("Collaboration server (Hocuspocus)") -- REST API <--> Back
Front -- OIDC --> Back -- OIDC ---> OIDC("Keycloak / ProConnect")
Back -- REST API --> Yserver
Back --> DB("Database (PostgreSQL)")
Back <--> Celery --> DB
Back ----> S3("Minio (S3)")
```
### Architecture decision records
- [ADR-0001-20250106-use-yjs-for-docs-editing](./adr/ADR-0001-20250106-use-yjs-for-docs-editing.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -4,7 +4,7 @@ Here we describe all environment variables that can be set for the docs applicat
## impress-backend container
These are the environment variables you can set for the `impress-backend` container.
These are the environmental variables you can set for the impress-backend container.
| Option | Description | default |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
@@ -39,7 +39,7 @@ These are the environment variables you can set for the `impress-backend` contai
| DJANGO_EMAIL_PORT | port used to connect to email host | |
| DJANGO_EMAIL_USE_TLS | use tls for email host connection | false |
| DJANGO_EMAIL_USE_SSL | use sstl for email host connection | false |
| DJANGO_EMAIL_FROM | email address used as sender | from@example.com |
| DJANGO_EMAIL_FROM | email adress used as sender | from@example.com |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | true |
| DJANGO_CORS_ALLOWED_ORIGINS | list of origins allowed for CORS | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | list of origins allowed for CORS using regulair expressions | [] |
@@ -47,25 +47,21 @@ These are the environment variables you can set for the `impress-backend` contai
| COLLABORATION_API_URL | collaboration api host | |
| COLLABORATION_SERVER_SECRET | collaboration api secret | |
| COLLABORATION_WS_URL | collaboration websocket url | |
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
| FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
| FRONTEND_THEME | frontend theme to use | |
| POSTHOG_KEY | posthog key for analytics | |
| CRISP_WEBSITE_ID | crisp website id for support | |
| DJANGO_CELERY_BROKER_URL | celery broker url | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker transport options | {} |
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
| OIDC_CREATE_USER | create used on OIDC | false |
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
| OIDC_RP_CLIENT_ID | client id used for OIDC | impress |
| OIDC_RP_CLIENT_SECRET | client secret used for OIDC | |
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | Autorization endpoint for OIDC | |
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth paramaters | {} |
| OIDC_RP_SCOPES | scopes requested for OIDC | openid email |
| LOGIN_REDIRECT_URL | login redirect url | |
| LOGIN_REDIRECT_URL_FAILURE | login redirect url on failure | |
@@ -75,16 +71,15 @@ These are the environment variables you can set for the `impress-backend` contai
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | faillback to email for identification | true |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow dupplicate emails | false |
| USER_OIDC_ESSENTIAL_CLAIMS | essential claims in OIDC token | [] |
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
| USER_OIDC_FIELDS_TO_FULLNAME | OIDC token claims to create full name | ["first_name", "last_name"] |
| USER_OIDC_FIELD_TO_SHORTNAME | OIDC token claims to create shortname | first_name |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_MODEL | AI Model to use | |
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_FEATURE_ENABLED | Enable AI options | false |
| Y_PROVIDER_API_KEY | Y provider API key | |
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown |
@@ -97,47 +92,3 @@ These are the environment variables you can set for the `impress-backend` contai
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
| REDIS_URL | cache url | redis://redis:6379/1 |
| CACHES_DEFAULT_TIMEOUT | cache default timeout | 30 |
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
| THEME_CUSTOMIZATION_FILE_PATH | full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
## impress-frontend image
These are the environment variables you can set to build the `impress-frontend` image.
Depending on how you are building the front-end application, this variable is used in different ways.
If you want to build the Docker image, this variable is used as an argument in the build command.
Example:
```
docker build -f src/frontend/Dockerfile --target frontend-production --build-arg PUBLISH_AS_MIT=false docs-frontend:latest
```
If you want to build the front-end application using the yarn build command, you can edit the file `src/frontend/apps/impress/.env` with the `NODE_ENV=production` environment variable and modify it. Alternatively, you can use the listed environment variables with the prefix `NEXT_PUBLIC_` (for example, `NEXT_PUBLIC_PUBLISH_AS_MIT=false`).
Example:
```
cd src/frontend/apps/impress
NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
```
| Option | Description | default |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| API_ORIGIN | backend domain - it uses the current domain if not initialized | |
| SW_DEACTIVATED | To not install the service worker | |
| 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`: [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)
In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features.
⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your [BlockNote licensing](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE) or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.

View File

@@ -33,8 +33,8 @@ backend:
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_VERIFY_SSL: False
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
USER_OIDC_FIELD_TO_SHORTNAME: "given_name"
USER_OIDC_FIELDS_TO_FULLNAME: "given_name,usual_name"
OIDC_REDIRECT_ALLOWED_HOSTS: https://impress.127.0.0.1.nip.io
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
@@ -82,13 +82,13 @@ backend:
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumes:
- name: certs
configMap:

View File

@@ -1,20 +1,20 @@
# Installation on a k8s cluster
This document is a step-by-step guide that describes how to install Docs on a k8s cluster without AI features. It's a teaching document to learn how it works. It needs to be adapted for a production environment.
This document is a step-by-step guide that describes how to install Docs on a k8s cluster without AI features. It's a teaching document to learn how it's work. It needs to be adapt for production environment.
## Prerequisites
- k8s cluster with an nginx-ingress controller
- an OIDC provider (if you don't have one, we provide an example)
- a PostgreSQL server (if you don't have one, we provide an example)
- a Memcached server (if you don't have one, we provide an example)
- a S3 bucket (if you don't have one, we provide an example)
- an OIDC provider (if you don't have one, we will provide an example)
- a PostgreSQL server (if you don't have one, we will provide an example)
- a Memcached server (if you don't have one, we will provide an example)
- a S3 bucket (if you don't have one, we will provide an example)
### Test cluster
If you do not have a test cluster, you can install everything on a local Kind cluster. In this case, the simplest way is to use our script **bin/start-kind.sh**.
If you do not have a test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script **bin/start-kind.sh**.
To be able to use the script, you need to install:
To be able to use the script, you will need to install:
- Docker (https://docs.docker.com/desktop/)
- Kind (https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
@@ -96,13 +96,13 @@ ingress-nginx-admission-patch-94dvt 0/1 Completed 1 2m56s
ingress-nginx-controller-57c548c4cd-2rx47 1/1 Running 0 2m56s
```
When your k8s cluster is ready (the ingress nginx controller is up), you can start the deployment. This cluster is special because it uses the `*.127.0.0.1.nip.io` domain and mkcert certificates to have full HTTPS support and easy domain name management.
When your k8s cluster is ready (the ingress nginx controller is up), you can start the deployment. This cluster is special because it uses the \*.127.0.0.1.nip.io domain and mkcert certificates to have full HTTPS support and easy domain name management.
Please remember that `*.127.0.0.1.nip.io` will always resolve to `127.0.0.1`, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
Please remember that \*.127.0.0.1.nip.io will always resolve to 127.0.0.1, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
## Preparation
### What do you use to authenticate your users?
### What will you use to authenticate your users ?
Docs uses OIDC, so if you already have an OIDC provider, obtain the necessary information to use it. In the next step, we will see how to configure Django (and thus Docs) to use it. If you do not have a provider, we will show you how to deploy a local Keycloak instance (this is not a production deployment, just a demo).
@@ -117,9 +117,9 @@ keycloak-0 1/1 Running 0 6m48s
keycloak-postgresql-0 1/1 Running 0 6m48s
```
From here the important information you will need are:
From here the important informations you will need are :
```yaml
```
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
@@ -133,9 +133,9 @@ OIDC_RP_SCOPES: "openid email"
You can find these values in **examples/keycloak.values.yaml**
### Find redis server connection values
### Find redis server connexion values
Docs needs a redis so we start by deploying one:
Impress need a redis so we will start by deploying a redis :
```
$ helm install redis oci://registry-1.docker.io/bitnamicharts/redis -f examples/redis.values.yaml
@@ -146,9 +146,9 @@ keycloak-postgresql-0 1/1 Running 0 26m
redis-master-0 1/1 Running 0 35s
```
### Find postgresql connection values
### Find postgresql connexion values
Docs uses a postgresql database as backend, so if you have a provider, obtain the necessary information to use it. If you don't, you can install a postgresql testing environment as follow:
Impress uses a postgresql db as backend so if you have a provider, obtain the necessary information to use it. If you do not have, you can install a postgresql testing environment as follow:
```
$ helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql -f examples/postgresql.values.yaml
@@ -160,9 +160,9 @@ postgresql-0 1/1 Running 0 14m
redis-master-0 1/1 Running 0 42s
```
From here the important information you will need are:
From here important informations you will need are :
```yaml
```
DB_HOST: postgres-postgresql
DB_NAME: impress
DB_USER: dinum
@@ -173,9 +173,9 @@ POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
### Find s3 bucket connection values
### Find s3 bucket connexion values
Docs uses an s3 bucket to store documents, so if you have a provider obtain the necessary information to use it. If you don't, you can install a local minio testing environment as follow:
Impress uses a s3 bucket to store documents so if you have a provider obtain the necessary information to use it. If you do not have, you can install a local minio testing environment as follow:
```
$ helm install minio oci://registry-1.docker.io/bitnamicharts/minio -f examples/minio.values.yaml
@@ -191,7 +191,7 @@ redis-master-0 1/1 Running 0 10m
## Deployment
Now you are ready to deploy Docs without AI. AI requires more dependencies (OpenAI API). To deploy Docs you need to provide all previous information to the helm chart.
Now you are ready to deploy Impress without AI. AI requiered more dependancies (openai API). To deploy impress you need to provide all previous informations to the helm chart.
```
$ helm repo add impress https://suitenumerique.github.io/docs/
@@ -214,7 +214,7 @@ redis-master-0 1/1 Running 0 20m
## Test your deployment
In order to test your deployment you have to log into your instance. If you exclusively use our examples you can do:
In order to test your deployment you have to login to your instance. If you use exclusively our examples you can do :
```
$ kubectl get ingress
@@ -227,4 +227,4 @@ impress-docs-ws <none> impress.127.0.0.1.nip.io localhost
keycloak <none> keycloak.127.0.0.1.nip.io localhost 80 49m
```
You can use Docs at https://impress.127.0.0.1.nip.io. The provisionning user in keycloak is impress/impress.
You can use impress on https://impress.127.0.0.1.nip.io. The provisionning user in keycloak is impress/impress.

View File

@@ -1,56 +0,0 @@
# Runtime Theming 🎨
### How to Use
To use this feature, simply set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. For example:
```javascript
FRONTEND_CSS_URL=http://anything/custom-style.css
```
Once you've set this variable, our application will load your custom CSS file and apply the styles to our frontend application.
### Benefits
This feature provides several benefits, including:
* **Easy customization** 🔄: With this feature, you can easily customize the look and feel of our application without requiring any code changes.
* **Flexibility** 🌈: You can use any CSS styles you like to create a custom theme that meets your needs.
* **Runtime theming** ⏱️: This feature allows you to change the theme of our application at runtime, without requiring a restart or recompilation.
### Example Use Case
Let's say you want to change the background color of our application to a custom color. You can create a custom CSS file with the following contents:
```css
body {
background-color: #3498db;
}
```
Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the background color to the custom color you specified.
----
# **Footer Configuration** 📝
The footer is configurable from the theme customization file.
### Settings 🔧
```shellscript
THEME_CUSTOMIZATION_FILE_PATH=<path>
```
### Example of JSON
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
`footer.default` is the fallback if the language is not supported.
---
Below is a visual example of a configured footer ⬇️:
![Footer Configuration Example](./assets/footer-configurable.png)

View File

@@ -50,7 +50,6 @@ OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# AI
AI_FEATURE_ENABLED=true
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama
@@ -61,3 +60,6 @@ COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
COLLABORATION_SERVER_SECRET=my-secret
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
# Frontend
FRONTEND_THEME=default

View File

@@ -1,6 +1,6 @@
# For the CI job test-e2e
SUSTAINED_THROTTLE_RATES="200/hour"
BURST_THROTTLE_RATES="200/minute"
DJANGO_SERVER_TO_SERVER_API_TOKENS=test-e2e
SUSTAINED_THROTTLE_RATES="200/hour"
Y_PROVIDER_API_KEY=yprovider-api-key
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/

View File

@@ -9,25 +9,11 @@
"matchManagers": ["pep621"],
"matchPackageNames": []
},
{
"groupName": "allowed django versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["Django"],
"allowedVersions": "<5.2"
},
{
"groupName": "allowed redis versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["redis"],
"allowedVersions": "<6.0.0"
},
{
"enabled": false,
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@hocuspocus/provider",
"@hocuspocus/server",
"eslint",
"fetch-mock",
"node",

View File

View File

@@ -1,7 +1,5 @@
"""API filters for Impress' core application."""
import unicodedata
from django.utils.translation import gettext_lazy as _
import django_filters
@@ -9,42 +7,13 @@ import django_filters
from core import models
def remove_accents(value):
"""Remove accents from a string (vélo -> velo)."""
return "".join(
c
for c in unicodedata.normalize("NFD", value)
if unicodedata.category(c) != "Mn"
)
class AccentInsensitiveCharFilter(django_filters.CharFilter):
"""
A custom CharFilter that filters on the accent-insensitive value searched.
"""
def filter(self, qs, value):
"""
Apply the filter to the queryset using the unaccented version of the field.
Args:
qs: The queryset to filter.
value: The value to search for in the unaccented field.
Returns:
A filtered queryset.
"""
if value:
value = remove_accents(value)
return super().filter(qs, value)
class DocumentFilter(django_filters.FilterSet):
"""
Custom filter for filtering documents on title (accent and case insensitive).
Custom filter for filtering documents.
"""
title = AccentInsensitiveCharFilter(
field_name="title", lookup_expr="unaccent__icontains", label=_("Title")
title = django_filters.CharFilter(
field_name="title", lookup_expr="icontains", label=_("Title")
)
class Meta:

View File

@@ -1,31 +1,27 @@
"""API endpoints"""
# pylint: disable=too-many-lines
import json
import logging
import uuid
from urllib.parse import unquote, urlencode, urlparse
from urllib.parse import unquote, urlparse
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
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.db import connection, transaction
from django.db import models as db
from django.db import transaction
from django.db.models.expressions import RawSQL
from django.db.models.functions import Left, Length
from django.http import Http404, StreamingHttpResponse
from django.urls import reverse
from django.utils.text import capfirst, slugify
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
import requests
import rest_framework as drf
from botocore.exceptions import ClientError
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
@@ -577,7 +573,7 @@ class DocumentViewSet(
queryset, filter_data["is_favorite"]
)
# Apply ordering only now that everything is filtered and annotated
# Apply ordering only now that everyting is filtered and annotated
queryset = filters.OrderingFilter().filter_queryset(
self.request, queryset, self
)
@@ -608,14 +604,6 @@ class DocumentViewSet(
@transaction.atomic
def perform_create(self, serializer):
"""Set the current user as creator and owner of the newly created object."""
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
obj = models.Document.add_root(
creator=self.request.user,
**serializer.validated_data,
@@ -675,19 +663,10 @@ class DocumentViewSet(
permission_classes=[],
url_path="create-for-owner",
)
@transaction.atomic
def create_for_owner(self, request):
"""
Create a document on behalf of a specified owner (pre-existing user or invited).
"""
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
# Deserialize and validate the data
serializer = serializers.ServerCreateDocumentSerializer(data=request.data)
if not serializer.is_valid():
@@ -793,12 +772,7 @@ class DocumentViewSet(
serializer.is_valid(raise_exception=True)
with transaction.atomic():
# "select_for_update" locks the table to ensure safe concurrent access
locked_parent = models.Document.objects.select_for_update().get(
pk=document.pk
)
child_document = locked_parent.add_child(
child_document = document.add_child(
creator=request.user,
**serializer.validated_data,
)
@@ -890,7 +864,7 @@ class DocumentViewSet(
)
# Compute cache for ancestors links to avoid many queries while computing
# abilities for his documents in the tree!
# abilties for his documents in the tree!
ancestors_links.append(
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
)
@@ -1157,10 +1131,7 @@ class DocumentViewSet(
# Prepare metadata for storage
extra_args = {
"Metadata": {
"owner": str(request.user.id),
"status": enums.DocumentAttachmentStatus.PROCESSING,
},
"Metadata": {"owner": str(request.user.id)},
"ContentType": serializer.validated_data["content_type"],
}
file_unsafe = ""
@@ -1192,18 +1163,8 @@ class DocumentViewSet(
document.attachments.append(key)
document.save()
malware_detection.analyse_file(key, document_id=document.id)
url = reverse(
"documents-media-check",
kwargs={"pk": document.id},
)
parameters = urlencode({"key": key})
return drf.response.Response(
{
"file": f"{url:s}?{parameters:s}",
},
{"file": f"{settings.MEDIA_URL:s}{key:s}"},
status=drf.status.HTTP_201_CREATED,
)
@@ -1285,71 +1246,11 @@ class DocumentViewSet(
logger.debug("User '%s' lacks permission for attachment", user)
raise drf.exceptions.PermissionDenied()
# Check if the attachment is ready
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
try:
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
except ClientError as err:
raise drf.exceptions.PermissionDenied() from err
metadata = head_resp.get("Metadata", {})
# In order to be compatible with existing upload without `status` metadata,
# we consider them as ready.
if (
metadata.get("status", enums.DocumentAttachmentStatus.READY)
!= enums.DocumentAttachmentStatus.READY
):
raise drf.exceptions.PermissionDenied()
# Generate S3 authorization headers using the extracted URL parameters
request = utils.generate_s3_authorization_headers(key)
return drf.response.Response("authorized", headers=request.headers, status=200)
@drf.decorators.action(detail=True, methods=["get"], url_path="media-check")
def media_check(self, request, *args, **kwargs):
"""
Check if the media is ready to be served.
"""
document = self.get_object()
key = request.query_params.get("key")
if not key:
return drf.response.Response(
{"detail": "Missing 'key' query parameter"},
status=drf.status.HTTP_400_BAD_REQUEST,
)
if key not in document.attachments:
return drf.response.Response(
{"detail": "Attachment missing"},
status=drf.status.HTTP_404_NOT_FOUND,
)
# Check if the attachment is ready
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
try:
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
except ClientError as err:
logger.error("Client Error fetching file %s metadata: %s", key, err)
return drf.response.Response(
{"detail": "Media not found"},
status=drf.status.HTTP_404_NOT_FOUND,
)
metadata = head_resp.get("Metadata", {})
body = {
"status": metadata.get("status", enums.DocumentAttachmentStatus.PROCESSING),
}
if metadata.get("status") == enums.DocumentAttachmentStatus.READY:
body = {
"status": enums.DocumentAttachmentStatus.READY,
"file": f"{settings.MEDIA_URL:s}{key:s}",
}
return drf.response.Response(body, status=drf.status.HTTP_200_OK)
@drf.decorators.action(
detail=True,
methods=["post"],
@@ -1783,13 +1684,9 @@ class ConfigView(drf.views.APIView):
Return a dictionary of public settings.
"""
array_settings = [
"AI_FEATURE_ENABLED",
"COLLABORATION_WS_URL",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
"CRISP_WEBSITE_ID",
"ENVIRONMENT",
"FRONTEND_CSS_URL",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
"FRONTEND_THEME",
"MEDIA_BASE_URL",
"POSTHOG_KEY",
@@ -1802,41 +1699,4 @@ class ConfigView(drf.views.APIView):
if hasattr(settings, setting):
dict_settings[setting] = getattr(settings, setting)
dict_settings["theme_customization"] = self._load_theme_customization()
return drf.response.Response(dict_settings)
def _load_theme_customization(self):
if not settings.THEME_CUSTOMIZATION_FILE_PATH:
return {}
cache_key = (
f"theme_customization_{slugify(settings.THEME_CUSTOMIZATION_FILE_PATH)}"
)
theme_customization = cache.get(cache_key, {})
if theme_customization:
return theme_customization
try:
with open(
settings.THEME_CUSTOMIZATION_FILE_PATH, "r", encoding="utf-8"
) as f:
theme_customization = json.load(f)
except FileNotFoundError:
logger.error(
"Configuration file not found: %s",
settings.THEME_CUSTOMIZATION_FILE_PATH,
)
except json.JSONDecodeError:
logger.error(
"Configuration file is not a valid JSON: %s",
settings.THEME_CUSTOMIZATION_FILE_PATH,
)
else:
cache.set(
cache_key,
theme_customization,
settings.THEME_CUSTOMIZATION_CACHE_TIMEOUT,
)
return theme_customization

View File

@@ -1,59 +1,130 @@
"""Authentication Backends for the Impress core app."""
import logging
import os
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.utils.translation import gettext_lazy as _
from lasuite.oidc_login.backends import (
OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend,
import requests
from mozilla_django_oidc.auth import (
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
)
from core.models import DuplicateEmailError
from core.models import DuplicateEmailError, User
logger = logging.getLogger(__name__)
# Settings renamed warnings
if os.environ.get("USER_OIDC_FIELDS_TO_FULLNAME"):
logger.warning(
"USER_OIDC_FIELDS_TO_FULLNAME has been renamed to "
"OIDC_USERINFO_FULLNAME_FIELDS please update your settings."
)
if os.environ.get("USER_OIDC_FIELD_TO_SHORTNAME"):
logger.warning(
"USER_OIDC_FIELD_TO_SHORTNAME has been renamed to "
"OIDC_USERINFO_SHORTNAME_FIELD please update your settings."
)
class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
"""Custom OpenID Connect (OIDC) Authentication Backend.
This class overrides the default OIDC Authentication Backend to accommodate differences
in the User and Identity models, and handles signed and/or encrypted UserInfo response.
"""
def get_extra_claims(self, user_info):
"""
Return extra claims from user_info.
def get_userinfo(self, access_token, id_token, payload):
"""Return user details dictionary.
Args:
user_info (dict): The user information dictionary.
Parameters:
- access_token (str): The access token.
- id_token (str): The id token (unused).
- payload (dict): The token payload (unused).
Note: The id_token and payload parameters are unused in this implementation,
but were kept to preserve base method signature.
Note: It handles signed and/or encrypted UserInfo Response. It is required by
Agent Connect, which follows the OIDC standard. It forces us to override the
base method, which deal with 'application/json' response.
Returns:
dict: A dictionary of extra claims.
- dict: User details dictionary obtained from the OpenID Connect user endpoint.
"""
return {
"full_name": self.compute_full_name(user_info),
"short_name": user_info.get(settings.OIDC_USERINFO_SHORTNAME_FIELD),
}
def get_existing_user(self, sub, email):
"""Fetch existing user by sub or email."""
user_response = requests.get(
self.OIDC_OP_USER_ENDPOINT,
headers={"Authorization": f"Bearer {access_token}"},
verify=self.get_settings("OIDC_VERIFY_SSL", True),
timeout=self.get_settings("OIDC_TIMEOUT", None),
proxies=self.get_settings("OIDC_PROXY", None),
)
user_response.raise_for_status()
try:
return self.UserModel.objects.get_user_by_sub_or_email(sub, email)
userinfo = user_response.json()
except ValueError:
try:
userinfo = self.verify_token(user_response.text)
except Exception as e:
raise SuspiciousOperation(
_("Invalid response format or token verification failed")
) from e
return userinfo
def verify_claims(self, claims):
"""
Verify the presence of essential claims and the "sub" (which is mandatory as defined
by the OIDC specification) to decide if authentication should be allowed.
"""
essential_claims = settings.USER_OIDC_ESSENTIAL_CLAIMS
missing_claims = [claim for claim in essential_claims if claim not in claims]
if missing_claims:
logger.error("Missing essential claims: %s", missing_claims)
return False
return True
def get_or_create_user(self, access_token, id_token, payload):
"""Return a User based on userinfo. Create a new user if no match is found."""
user_info = self.get_userinfo(access_token, id_token, payload)
if not self.verify_claims(user_info):
raise SuspiciousOperation("Claims verification failed.")
sub = user_info["sub"]
email = user_info.get("email")
# Get user's full name from OIDC fields defined in settings
full_name = self.compute_full_name(user_info)
short_name = user_info.get(settings.USER_OIDC_FIELD_TO_SHORTNAME)
claims = {
"email": email,
"full_name": full_name,
"short_name": short_name,
}
try:
user = User.objects.get_user_by_sub_or_email(sub, email)
except DuplicateEmailError as err:
raise SuspiciousOperation(err.message) from err
if user:
if not user.is_active:
raise SuspiciousOperation(_("User account is disabled"))
self.update_user_if_needed(user, claims)
elif self.get_settings("OIDC_CREATE_USER", True):
user = User.objects.create(sub=sub, password="!", **claims) # noqa: S106
return user
def compute_full_name(self, user_info):
"""Compute user's full name based on OIDC fields in settings."""
name_fields = settings.USER_OIDC_FIELDS_TO_FULLNAME
full_name = " ".join(
user_info[field] for field in name_fields if user_info.get(field)
)
return full_name or None
def update_user_if_needed(self, user, claims):
"""Update user claims if they have changed."""
has_changed = any(
value and value != getattr(user, key) for key, value in claims.items()
)
if has_changed:
updated_claims = {key: value for key, value in claims.items() if value}
self.UserModel.objects.filter(id=user.id).update(**updated_claims)

View File

@@ -0,0 +1,18 @@
"""Authentication URLs for the People core app."""
from django.urls import path
from mozilla_django_oidc.urls import urlpatterns as mozzila_oidc_urls
from .views import OIDCLogoutCallbackView, OIDCLogoutView
urlpatterns = [
# Override the default 'logout/' path from Mozilla Django OIDC with our custom view.
path("logout/", OIDCLogoutView.as_view(), name="oidc_logout_custom"),
path(
"logout-callback/",
OIDCLogoutCallbackView.as_view(),
name="oidc_logout_callback",
),
*mozzila_oidc_urls,
]

View File

@@ -0,0 +1,137 @@
"""Authentication Views for the People core app."""
from urllib.parse import urlencode
from django.contrib import auth
from django.core.exceptions import SuspiciousOperation
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils import crypto
from mozilla_django_oidc.utils import (
absolutify,
)
from mozilla_django_oidc.views import (
OIDCLogoutView as MozillaOIDCOIDCLogoutView,
)
class OIDCLogoutView(MozillaOIDCOIDCLogoutView):
"""Custom logout view for handling OpenID Connect (OIDC) logout flow.
Adds support for handling logout callbacks from the identity provider (OP)
by initiating the logout flow if the user has an active session.
The Django session is retained during the logout process to persist the 'state' OIDC parameter.
This parameter is crucial for maintaining the integrity of the logout flow between this call
and the subsequent callback.
"""
@staticmethod
def persist_state(request, state):
"""Persist the given 'state' parameter in the session's 'oidc_states' dictionary
This method is used to store the OIDC state parameter in the session, according to the
structure expected by Mozilla Django OIDC's 'add_state_and_verifier_and_nonce_to_session'
utility function.
"""
if "oidc_states" not in request.session or not isinstance(
request.session["oidc_states"], dict
):
request.session["oidc_states"] = {}
request.session["oidc_states"][state] = {}
request.session.save()
def construct_oidc_logout_url(self, request):
"""Create the redirect URL for interfacing with the OIDC provider.
Retrieves the necessary parameters from the session and constructs the URL
required to initiate logout with the OpenID Connect provider.
If no ID token is found in the session, the logout flow will not be initiated,
and the method will return the default redirect URL.
The 'state' parameter is generated randomly and persisted in the session to ensure
its integrity during the subsequent callback.
"""
oidc_logout_endpoint = self.get_settings("OIDC_OP_LOGOUT_ENDPOINT")
if not oidc_logout_endpoint:
return self.redirect_url
reverse_url = reverse("oidc_logout_callback")
id_token = request.session.get("oidc_id_token", None)
if not id_token:
return self.redirect_url
query = {
"id_token_hint": id_token,
"state": crypto.get_random_string(self.get_settings("OIDC_STATE_SIZE", 32)),
"post_logout_redirect_uri": absolutify(request, reverse_url),
}
self.persist_state(request, query["state"])
return f"{oidc_logout_endpoint}?{urlencode(query)}"
def post(self, request):
"""Handle user logout.
If the user is not authenticated, redirects to the default logout URL.
Otherwise, constructs the OIDC logout URL and redirects the user to start
the logout process.
If the user is redirected to the default logout URL, ensure her Django session
is terminated.
"""
logout_url = self.redirect_url
if request.user.is_authenticated:
logout_url = self.construct_oidc_logout_url(request)
# If the user is not redirected to the OIDC provider, ensure logout
if logout_url == self.redirect_url:
auth.logout(request)
return HttpResponseRedirect(logout_url)
class OIDCLogoutCallbackView(MozillaOIDCOIDCLogoutView):
"""Custom view for handling the logout callback from the OpenID Connect (OIDC) provider.
Handles the callback after logout from the identity provider (OP).
Verifies the state parameter and performs necessary logout actions.
The Django session is maintained during the logout process to ensure the integrity
of the logout flow initiated in the previous step.
"""
http_method_names = ["get"]
def get(self, request):
"""Handle the logout callback.
If the user is not authenticated, redirects to the default logout URL.
Otherwise, verifies the state parameter and performs necessary logout actions.
"""
if not request.user.is_authenticated:
return HttpResponseRedirect(self.redirect_url)
state = request.GET.get("state")
if state not in request.session.get("oidc_states", {}):
msg = "OIDC callback state not found in session `oidc_states`!"
raise SuspiciousOperation(msg)
del request.session["oidc_states"][state]
request.session.save()
auth.logout(request)
return HttpResponseRedirect(self.redirect_url)

View File

@@ -3,7 +3,6 @@ Core application enums declaration
"""
import re
from enum import StrEnum
from django.conf import global_settings, settings
from django.db import models
@@ -39,10 +38,3 @@ class MoveNodePositionChoices(models.TextChoices):
LAST_SIBLING = "last-sibling", _("Last sibling")
LEFT = "left", _("Left")
RIGHT = "right", _("Right")
class DocumentAttachmentStatus(StrEnum):
"""Defines the possible statuses for an attachment."""
PROCESSING = "processing"
READY = "ready"

View File

@@ -1,52 +0,0 @@
"""Malware detection callbacks"""
import logging
from django.core.files.storage import default_storage
from lasuite.malware_detection.enums import ReportStatus
from core.enums import DocumentAttachmentStatus
from core.models import Document
logger = logging.getLogger(__name__)
security_logger = logging.getLogger("docs.security")
def malware_detection_callback(file_path, status, error_info, **kwargs):
"""Malware detection callback"""
if status == ReportStatus.SAFE:
logger.info("File %s is safe", file_path)
# Get existing metadata
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
head_resp = s3_client.head_object(Bucket=bucket_name, Key=file_path)
metadata = head_resp.get("Metadata", {})
metadata.update({"status": DocumentAttachmentStatus.READY})
# Update status in metadata
s3_client.copy_object(
Bucket=bucket_name,
CopySource={"Bucket": bucket_name, "Key": file_path},
Key=file_path,
ContentType=head_resp.get("ContentType"),
Metadata=metadata,
MetadataDirective="REPLACE",
)
return
document_id = kwargs.get("document_id")
security_logger.warning(
"File %s for document %s is infected with malware. Error info: %s",
file_path,
document_id,
error_info,
)
# Remove the file from the document and change the status to unsafe
document = Document.objects.get(pk=document_id)
document.attachments.remove(file_path)
document.save(update_fields=["attachments"])
# Delete the file from the storage
default_storage.delete(file_path)

View File

@@ -1,10 +0,0 @@
from django.contrib.postgres.operations import UnaccentExtension
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0020_remove_is_public_add_field_attachments_and_duplicated_from"),
]
operations = [UnaccentExtension()]

View File

@@ -835,7 +835,6 @@ class Document(MP_Node, BaseModel):
"ai_transform": ai_access,
"ai_translate": ai_access,
"attachment_upload": can_update,
"media_check": can_get,
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,

View File

@@ -44,7 +44,7 @@ AI_ACTIONS = {
}
AI_TRANSLATE = (
"Keep the same html structure and formatting. "
"Keep the same html stucture and formatting. "
"Translate the content in the html to the specified language {language:s}. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."

View File

@@ -17,7 +17,7 @@ class CollaborationService:
def reset_connections(self, room, user_id=None):
"""
Reset connections of a room in the collaboration server.
Resetting a connection means that the user will be disconnected and will
Reseting a connection means that the user will be disconnected and will
have to reconnect to the collaboration server, with updated rights.
"""
endpoint = "reset-connections"

View File

@@ -2,14 +2,14 @@
import random
import re
from logging import Logger
from unittest import mock
from django.core.exceptions import SuspiciousOperation
from django.test.utils import override_settings
import pytest
import responses
from cryptography.fernet import Fernet
from lasuite.oidc_login.backends import get_oidc_refresh_token
from core import models
from core.authentication.backends import OIDCAuthenticationBackend
@@ -57,7 +57,7 @@ def test_authentication_getter_existing_user_via_email(
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with django_assert_num_queries(3): # user by sub, user by mail, update sub
with django_assert_num_queries(2):
user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
@@ -288,7 +288,7 @@ def test_authentication_getter_new_user_no_email(monkeypatch):
assert user.email is None
assert user.full_name is None
assert user.short_name is None
assert user.has_usable_password() is False
assert user.password == "!"
assert models.User.objects.count() == 1
@@ -315,7 +315,7 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
assert user.email == email
assert user.full_name == "John Doe"
assert user.short_name == "John"
assert user.has_usable_password() is False
assert user.password == "!"
assert models.User.objects.count() == 1
@@ -345,15 +345,11 @@ def test_authentication_get_userinfo_json_response():
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
@responses.activate
def test_authentication_get_userinfo_token_response(monkeypatch, settings):
def test_authentication_get_userinfo_token_response(monkeypatch):
"""Test get_userinfo method with a token response."""
settings.OIDC_RP_SIGN_ALGO = "HS256" # disable JWKS URL call
responses.add(
responses.GET,
re.compile(r".*/userinfo"),
body="fake.jwt.token",
status=200,
content_type="application/jwt",
responses.GET, re.compile(r".*/userinfo"), body="fake.jwt.token", status=200
)
def mock_verify_token(self, token): # pylint: disable=unused-argument
@@ -375,25 +371,21 @@ def test_authentication_get_userinfo_token_response(monkeypatch, settings):
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
@responses.activate
def test_authentication_get_userinfo_invalid_response(settings):
def test_authentication_get_userinfo_invalid_response():
"""
Test get_userinfo method with an invalid JWT response that
causes verify_token to raise an error.
"""
settings.OIDC_RP_SIGN_ALGO = "HS256" # disable JWKS URL call
responses.add(
responses.GET,
re.compile(r".*/userinfo"),
body="fake.jwt.token",
status=200,
content_type="application/jwt",
responses.GET, re.compile(r".*/userinfo"), body="fake.jwt.token", status=200
)
oidc_backend = OIDCAuthenticationBackend()
with pytest.raises(
SuspiciousOperation,
match="User info response was not valid JWT",
match="Invalid response format or token verification failed",
):
oidc_backend.get_userinfo("fake_access_token", None, None)
@@ -458,54 +450,100 @@ def test_authentication_getter_existing_disabled_user_via_email(
assert models.User.objects.count() == 1
@responses.activate
def test_authentication_session_tokens(
django_assert_num_queries, monkeypatch, rf, settings
# Essential claims
def test_authentication_verify_claims_default(django_assert_num_queries, monkeypatch):
"""The sub claim should be mandatory by default."""
klass = OIDCAuthenticationBackend()
def get_userinfo_mocked(*args):
return {
"test": "123",
}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with (
django_assert_num_queries(0),
pytest.raises(
KeyError,
match="sub",
),
):
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
assert models.User.objects.exists() is False
@pytest.mark.parametrize(
"essential_claims, missing_claims",
[
(["email", "sub"], ["email"]),
(["Email", "sub"], ["Email"]), # Case sensitivity
],
)
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
@mock.patch.object(Logger, "error")
def test_authentication_verify_claims_essential_missing(
mock_logger,
essential_claims,
missing_claims,
django_assert_num_queries,
monkeypatch,
):
"""
Test that the session contains oidc_refresh_token and oidc_access_token after authentication.
"""
settings.OIDC_OP_TOKEN_ENDPOINT = "http://oidc.endpoint.test/token"
settings.OIDC_OP_USER_ENDPOINT = "http://oidc.endpoint.test/userinfo"
settings.OIDC_OP_JWKS_ENDPOINT = "http://oidc.endpoint.test/jwks"
settings.OIDC_STORE_ACCESS_TOKEN = True
settings.OIDC_STORE_REFRESH_TOKEN = True
settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()
"""Ensure SuspiciousOperation is raised if essential claims are missing."""
klass = OIDCAuthenticationBackend()
request = rf.get("/some-url", {"state": "test-state", "code": "test-code"})
request.session = {}
def verify_token_mocked(*args, **kwargs):
return {"sub": "123", "email": "test@example.com"}
def get_userinfo_mocked(*args):
return {
"sub": "123",
"last_name": "Doe",
}
monkeypatch.setattr(OIDCAuthenticationBackend, "verify_token", verify_token_mocked)
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
responses.add(
responses.POST,
re.compile(settings.OIDC_OP_TOKEN_ENDPOINT),
json={
"access_token": "test-access-token",
"refresh_token": "test-refresh-token",
},
status=200,
)
with (
django_assert_num_queries(0),
pytest.raises(
SuspiciousOperation,
match="Claims verification failed",
),
override_settings(USER_OIDC_ESSENTIAL_CLAIMS=essential_claims),
):
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
responses.add(
responses.GET,
re.compile(settings.OIDC_OP_USER_ENDPOINT),
json={"sub": "123", "email": "test@example.com"},
status=200,
)
assert models.User.objects.exists() is False
mock_logger.assert_called_once_with("Missing essential claims: %s", missing_claims)
@override_settings(
OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo",
USER_OIDC_ESSENTIAL_CLAIMS=["email", "last_name"],
)
def test_authentication_verify_claims_success(django_assert_num_queries, monkeypatch):
"""Ensure user is authenticated when all essential claims are present."""
klass = OIDCAuthenticationBackend()
def get_userinfo_mocked(*args):
return {
"email": "john.doe@example.com",
"last_name": "Doe",
"sub": "123",
}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with django_assert_num_queries(6):
user = klass.authenticate(
request,
code="test-code",
nonce="test-nonce",
code_verifier="test-code-verifier",
user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
assert user is not None
assert request.session["oidc_access_token"] == "test-access-token"
assert get_oidc_refresh_token(request.session) == "test-refresh-token"
assert models.User.objects.filter(id=user.id).exists()
assert user.sub == "123"
assert user.full_name == "Doe"
assert user.short_name is None
assert user.email == "john.doe@example.com"

View File

@@ -0,0 +1,10 @@
"""Unit tests for the Authentication URLs."""
from core.authentication.urls import urlpatterns
def test_urls_override_default_mozilla_django_oidc():
"""Custom URL patterns should override default ones from Mozilla Django OIDC."""
url_names = [u.name for u in urlpatterns]
assert url_names.index("oidc_logout_custom") < url_names.index("oidc_logout")

View File

@@ -0,0 +1,231 @@
"""Unit tests for the Authentication Views."""
from unittest import mock
from urllib.parse import parse_qs, urlparse
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.exceptions import SuspiciousOperation
from django.test import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from django.utils import crypto
import pytest
from rest_framework.test import APIClient
from core import factories
from core.authentication.views import OIDCLogoutCallbackView, OIDCLogoutView
pytestmark = pytest.mark.django_db
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
def test_view_logout_anonymous():
"""Anonymous users calling the logout url,
should be redirected to the specified LOGOUT_REDIRECT_URL."""
url = reverse("oidc_logout_custom")
response = APIClient().get(url)
assert response.status_code == 302
assert response.url == "/example-logout"
@mock.patch.object(
OIDCLogoutView, "construct_oidc_logout_url", return_value="/example-logout"
)
def test_view_logout(mocked_oidc_logout_url):
"""Authenticated users should be redirected to OIDC provider for logout."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
url = reverse("oidc_logout_custom")
response = client.get(url)
mocked_oidc_logout_url.assert_called_once()
assert response.status_code == 302
assert response.url == "/example-logout"
@override_settings(LOGOUT_REDIRECT_URL="/default-redirect-logout")
@mock.patch.object(
OIDCLogoutView, "construct_oidc_logout_url", return_value="/default-redirect-logout"
)
def test_view_logout_no_oidc_provider(mocked_oidc_logout_url):
"""Authenticated users should be logged out when no OIDC provider is available."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
url = reverse("oidc_logout_custom")
with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout:
response = client.get(url)
mocked_oidc_logout_url.assert_called_once()
mock_logout.assert_called_once()
assert response.status_code == 302
assert response.url == "/default-redirect-logout"
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
def test_view_logout_callback_anonymous():
"""Anonymous users calling the logout callback url,
should be redirected to the specified LOGOUT_REDIRECT_URL."""
url = reverse("oidc_logout_callback")
response = APIClient().get(url)
assert response.status_code == 302
assert response.url == "/example-logout"
@pytest.mark.parametrize(
"initial_oidc_states",
[{}, {"other_state": "foo"}],
)
def test_view_logout_persist_state(initial_oidc_states):
"""State value should be persisted in session's data."""
user = factories.UserFactory()
request = RequestFactory().request()
request.user = user
middleware = SessionMiddleware(get_response=lambda x: x)
middleware.process_request(request)
if initial_oidc_states:
request.session["oidc_states"] = initial_oidc_states
request.session.save()
mocked_state = "mock_state"
OIDCLogoutView().persist_state(request, mocked_state)
assert "oidc_states" in request.session
assert request.session["oidc_states"] == {
"mock_state": {},
**initial_oidc_states,
}
@override_settings(OIDC_OP_LOGOUT_ENDPOINT="/example-logout")
@mock.patch.object(OIDCLogoutView, "persist_state")
@mock.patch.object(crypto, "get_random_string", return_value="mocked_state")
def test_view_logout_construct_oidc_logout_url(
mocked_get_random_string, mocked_persist_state
):
"""Should construct the logout URL to initiate the logout flow with the OIDC provider."""
user = factories.UserFactory()
request = RequestFactory().request()
request.user = user
middleware = SessionMiddleware(get_response=lambda x: x)
middleware.process_request(request)
request.session["oidc_id_token"] = "mocked_oidc_id_token"
request.session.save()
redirect_url = OIDCLogoutView().construct_oidc_logout_url(request)
mocked_persist_state.assert_called_once()
mocked_get_random_string.assert_called_once()
params = parse_qs(urlparse(redirect_url).query)
assert params["id_token_hint"][0] == "mocked_oidc_id_token"
assert params["state"][0] == "mocked_state"
url = reverse("oidc_logout_callback")
assert url in params["post_logout_redirect_uri"][0]
@override_settings(LOGOUT_REDIRECT_URL="/")
def test_view_logout_construct_oidc_logout_url_none_id_token():
"""If no ID token is available in the session,
the user should be redirected to the final URL."""
user = factories.UserFactory()
request = RequestFactory().request()
request.user = user
middleware = SessionMiddleware(get_response=lambda x: x)
middleware.process_request(request)
redirect_url = OIDCLogoutView().construct_oidc_logout_url(request)
assert redirect_url == "/"
@pytest.mark.parametrize(
"initial_state",
[None, {"other_state": "foo"}],
)
def test_view_logout_callback_wrong_state(initial_state):
"""Should raise an error if OIDC state doesn't match session data."""
user = factories.UserFactory()
request = RequestFactory().request()
request.user = user
middleware = SessionMiddleware(get_response=lambda x: x)
middleware.process_request(request)
if initial_state:
request.session["oidc_states"] = initial_state
request.session.save()
callback_view = OIDCLogoutCallbackView.as_view()
with pytest.raises(SuspiciousOperation) as excinfo:
callback_view(request)
assert (
str(excinfo.value) == "OIDC callback state not found in session `oidc_states`!"
)
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
def test_view_logout_callback():
"""If state matches, callback should clear OIDC state and redirects."""
user = factories.UserFactory()
request = RequestFactory().get("/logout-callback/", data={"state": "mocked_state"})
request.user = user
middleware = SessionMiddleware(get_response=lambda x: x)
middleware.process_request(request)
mocked_state = "mocked_state"
request.session["oidc_states"] = {mocked_state: {}}
request.session.save()
callback_view = OIDCLogoutCallbackView.as_view()
with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout:
def clear_user(request):
# Assert state is cleared prior to logout
assert request.session["oidc_states"] == {}
request.user = AnonymousUser()
mock_logout.side_effect = clear_user
response = callback_view(request)
mock_logout.assert_called_once()
assert response.status_code == 302
assert response.url == "/example-logout"

View File

@@ -2,8 +2,6 @@
from unittest import mock
from django.core.cache import cache
import pytest
USER = "user"
@@ -11,12 +9,6 @@ TEAM = "team"
VIA = [USER, TEAM]
@pytest.fixture(autouse=True)
def clear_cache():
"""Fixture to clear the cache before each test."""
cache.clear()
@pytest.fixture
def mock_user_teams():
"""Mock for the "teams" property on the User model."""

View File

@@ -304,7 +304,7 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
)
elif expected_language == "fr-fr":
assert (
f"{user.full_name} a partagé un document avec vous : {document.title}".lower()
f"{user.full_name} a partagé un document avec vous: {document.title}".lower()
in email_subject.lower()
)
assert "docs/" + str(document.id) + "/" in email_content.lower()

View File

@@ -575,7 +575,7 @@ def test_api_document_invitations_create_cannot_invite_existing_users():
document = factories.DocumentFactory(users=[(user, "owner")])
existing_user = factories.UserFactory()
# Build an invitation to the email of an existing identity in the db
# Build an invitation to the email of an exising identity in the db
invitation_values = {
"email": existing_user.email,
"role": random.choice(models.RoleChoices.values),

View File

@@ -5,6 +5,7 @@ Test AI transform API endpoint for users in impress's core app.
import random
from unittest.mock import MagicMock, patch
from django.core.cache import cache
from django.test import override_settings
import pytest
@@ -16,6 +17,12 @@ from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def clear_cache():
"""Fixture to clear the cache before each test."""
cache.clear()
@pytest.fixture
def ai_settings():
"""Fixture to set AI settings."""
@@ -150,7 +157,7 @@ def test_api_documents_ai_transform_authenticated_forbidden(reach, role):
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_authenticated_success(mock_create, reach, role):
"""
Authenticated who are not related to a document should be able to request AI transform
Autenticated who are not related to a document should be able to request AI transform
if the link reach and role permit it.
"""
user = factories.UserFactory()

View File

@@ -5,6 +5,7 @@ Test AI translate API endpoint for users in impress's core app.
import random
from unittest.mock import MagicMock, patch
from django.core.cache import cache
from django.test import override_settings
import pytest
@@ -16,6 +17,12 @@ from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def clear_cache():
"""Fixture to clear the cache before each test."""
cache.clear()
@pytest.fixture
def ai_settings():
"""Fixture to set AI settings."""
@@ -99,7 +106,7 @@ def test_api_documents_ai_translate_anonymous_success(mock_create):
{
"role": "system",
"content": (
"Keep the same html structure and formatting. "
"Keep the same html stucture and formatting. "
"Translate the content in the html to the specified language Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
@@ -172,7 +179,7 @@ def test_api_documents_ai_translate_authenticated_forbidden(reach, role):
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_authenticated_success(mock_create, reach, role):
"""
Authenticated who are not related to a document should be able to request AI translate
Autenticated who are not related to a document should be able to request AI translate
if the link reach and role permit it.
"""
user = factories.UserFactory()
@@ -197,7 +204,7 @@ def test_api_documents_ai_translate_authenticated_success(mock_create, reach, ro
{
"role": "system",
"content": (
"Keep the same html structure and formatting. "
"Keep the same html stucture and formatting. "
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
@@ -274,7 +281,7 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te
{
"role": "system",
"content": (
"Keep the same html structure and formatting. "
"Keep the same html stucture and formatting. "
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "

View File

@@ -4,8 +4,6 @@ Test file uploads API endpoint for users in impress's core app.
import re
import uuid
from unittest import mock
from urllib.parse import parse_qs, urlparse
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -14,7 +12,6 @@ import pytest
from rest_framework.test import APIClient
from core import factories
from core.api.viewsets import malware_detection
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@@ -62,17 +59,12 @@ def test_api_documents_attachment_upload_anonymous_success():
file = SimpleUploadedFile(name="test.png", content=PIXEL, content_type="image/png")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
response = APIClient().post(url, {"file": file}, format="multipart")
response = APIClient().post(url, {"file": file}, format="multipart")
assert response.status_code == 201
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.png")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
file_path = response.json()["file"]
match = pattern.search(file_path)
file_id = match.group(1)
# Validate that file_id is a valid UUID
@@ -82,13 +74,12 @@ def test_api_documents_attachment_upload_anonymous_success():
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.png"]
# Now, check the metadata of the uploaded file
key = file_path.replace("/media/", "")
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
key = file_path.replace("/media", "")
file_head = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": "None", "status": "processing"}
assert file_head["Metadata"] == {"owner": "None"}
assert file_head["ContentType"] == "image/png"
assert file_head["ContentDisposition"] == 'inline; filename="test.png"'
@@ -136,7 +127,7 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
)
def test_api_documents_attachment_upload_authenticated_success(reach, role):
"""
Authenticated users who are not related to a document should be able to upload
Autenticated users who are not related to a document should be able to upload
a file when the link reach and role permit it.
"""
user = factories.UserFactory()
@@ -148,24 +139,14 @@ def test_api_documents_attachment_upload_authenticated_success(reach, role):
file = SimpleUploadedFile(name="test.png", content=PIXEL, content_type="image/png")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
response = client.post(url, {"file": file}, format="multipart")
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 201
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.png")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
match = pattern.search(file_path)
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
match = pattern.search(response.json()["file"])
file_id = match.group(1)
mock_analyse_file.assert_called_once_with(
f"{document.id!s}/attachments/{file_id!s}.png", document_id=document.id
)
# Validate that file_id is a valid UUID
uuid.UUID(file_id)
@@ -229,17 +210,12 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
file = SimpleUploadedFile(name="test.png", content=PIXEL, content_type="image/png")
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
response = client.post(url, {"file": file}, format="multipart")
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 201
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.png")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
file_path = response.json()["file"]
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
match = pattern.search(file_path)
file_id = match.group(1)
@@ -250,12 +226,11 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
assert document.attachments == [f"{document.id!s}/attachments/{file_id!s}.png"]
# Now, check the metadata of the uploaded file
key = file_path.replace("/media/", "")
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
key = file_path.replace("/media", "")
file_head = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": str(user.id), "status": "processing"}
assert file_head["Metadata"] == {"owner": str(user.id)}
assert file_head["ContentType"] == "image/png"
assert file_head["ContentDisposition"] == 'inline; filename="test.png"'
@@ -280,7 +255,7 @@ def test_api_documents_attachment_upload_invalid(client):
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
"""The uploaded file should not exceed the maximum size in settings."""
"""The uploaded file should not exceeed the maximum size in settings."""
settings.DOCUMENT_IMAGE_MAX_SIZE = 1048576 # 1 MB for test
user = factories.UserFactory()
@@ -329,18 +304,12 @@ def test_api_documents_attachment_upload_fix_extension(
url = f"/api/v1.0/documents/{document.id!s}/attachment-upload/"
file = SimpleUploadedFile(name=name, content=content)
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
response = client.post(url, {"file": file}, format="multipart")
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 201
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.{extension:s}")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
file_path = response.json()["file"]
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.{extension:s}")
match = pattern.search(file_path)
file_id = match.group(1)
@@ -355,16 +324,11 @@ def test_api_documents_attachment_upload_fix_extension(
uuid.UUID(file_id)
# Now, check the metadata of the uploaded file
key = file_path.replace("/media/", "")
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
key = file_path.replace("/media", "")
file_head = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {
"owner": str(user.id),
"is_unsafe": "true",
"status": "processing",
}
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
assert file_head["ContentType"] == content_type
assert file_head["ContentDisposition"] == f'attachment; filename="{name:s}"'
@@ -400,17 +364,12 @@ def test_api_documents_attachment_upload_unsafe():
file = SimpleUploadedFile(
name="script.exe", content=b"\x4d\x5a\x90\x00\x03\x00\x00\x00"
)
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
response = client.post(url, {"file": file}, format="multipart")
response = client.post(url, {"file": file}, format="multipart")
assert response.status_code == 201
pattern = re.compile(rf"^{document.id!s}/attachments/(.*)\.exe")
url_parsed = urlparse(response.json()["file"])
assert url_parsed.path == f"/api/v1.0/documents/{document.id!s}/media-check/"
query = parse_qs(url_parsed.query)
assert query["key"][0] is not None
file_path = query["key"][0]
file_path = response.json()["file"]
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.exe")
match = pattern.search(file_path)
file_id = match.group(1)
@@ -422,20 +381,11 @@ def test_api_documents_attachment_upload_unsafe():
file_id = file_id.replace("-unsafe", "")
uuid.UUID(file_id)
key = file_path.replace("/media/", "")
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
# Now, check the metadata of the uploaded file
key = file_path.replace("/media", "")
file_head = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {
"owner": str(user.id),
"is_unsafe": "true",
"status": "processing",
}
# Depending the libmagic version, the content type may change.
assert file_head["ContentType"] in [
"application/x-dosexec",
"application/octet-stream",
]
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
assert file_head["ContentType"] == "application/octet-stream"
assert file_head["ContentDisposition"] == 'attachment; filename="script.exe"'

View File

@@ -2,7 +2,6 @@
Tests for Documents API endpoint in impress's core app: children create
"""
from concurrent.futures import ThreadPoolExecutor
from uuid import uuid4
import pytest
@@ -250,41 +249,3 @@ def test_api_documents_children_create_force_id_existing():
assert response.json() == {
"id": ["A document with this ID already exists. You cannot override it."]
}
@pytest.mark.django_db(transaction=True)
def test_api_documents_create_document_children_race_condition():
"""
It should be possible to create several documents at the same time
without causing any race conditions or data integrity issues.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
factories.UserDocumentAccessFactory(user=user, document=document, role="owner")
def create_document():
return client.post(
f"/api/v1.0/documents/{document.id}/children/",
{
"title": "my child",
},
)
with ThreadPoolExecutor(max_workers=2) as executor:
future1 = executor.submit(create_document)
future2 = executor.submit(create_document)
response1 = future1.result()
response2 = future2.result()
assert response1.status_code == 201
assert response2.status_code == 201
document.refresh_from_db()
assert document.numchild == 2

View File

@@ -2,7 +2,6 @@
Tests for Documents API endpoint in impress's core app: create
"""
from concurrent.futures import ThreadPoolExecutor
from uuid import uuid4
import pytest
@@ -52,36 +51,6 @@ def test_api_documents_create_authenticated_success():
assert document.accesses.filter(role="owner", user=user).exists()
@pytest.mark.django_db(transaction=True)
def test_api_documents_create_document_race_condition():
"""
It should be possible to create several documents at the same time
without causing any race conditions or data integrity issues.
"""
def create_document(title):
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
return client.post(
"/api/v1.0/documents/",
{
"title": title,
},
format="json",
)
with ThreadPoolExecutor(max_workers=2) as executor:
future1 = executor.submit(create_document, "my document 1")
future2 = executor.submit(create_document, "my document 2")
response1 = future1.result()
response2 = future2.result()
assert response1.status_code == 201
assert response2.status_code == 201
def test_api_documents_create_authenticated_title_null():
"""It should be possible to create several documents with a null title."""
user = factories.UserFactory()

View File

@@ -4,7 +4,6 @@ Tests for Documents API endpoint in impress's core app: create
# pylint: disable=W0621
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import patch
from django.core import mail
@@ -279,7 +278,7 @@ def test_api_documents_create_for_owner_existing_user_email_no_sub_with_fallback
"""
It should be possible to create a document on behalf of a pre-existing user for
who the sub was not found if the settings allow it. This edge case should not
happen in a healthy OIDC federation but can be useful if an OIDC provider modifies
happen in a healthy OIDC federation but can be usefull if an OIDC provider modifies
users sub on each login for example...
"""
user = factories.UserFactory(language="en-us")
@@ -426,36 +425,6 @@ def test_api_documents_create_for_owner_new_user_no_sub_no_fallback_allow_duplic
assert document.creator == user
@pytest.mark.django_db(transaction=True)
def test_api_documents_create_document_race_condition():
"""
It should be possible to create several documents at the same time
without causing any race conditions or data integrity issues.
"""
def create_document(title):
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
return client.post(
"/api/v1.0/documents/",
{
"title": title,
},
format="json",
)
with ThreadPoolExecutor(max_workers=2) as executor:
future1 = executor.submit(create_document, "my document 1")
future2 = executor.submit(create_document, "my document 2")
response1 = future1.result()
response2 = future2.result()
assert response1.status_code == 201
assert response2.status_code == 201
@patch.object(ServerCreateDocumentSerializer, "_send_email_notification")
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"], LANGUAGE_CODE="de-de")
def test_api_documents_create_for_owner_with_default_language(

View File

@@ -7,7 +7,6 @@ from faker import Faker
from rest_framework.test import APIClient
from core import factories
from core.api.filters import remove_accents
fake = Faker()
pytestmark = pytest.mark.django_db
@@ -50,16 +49,14 @@ def test_api_documents_descendants_filter_unknown_field():
[
("Project Alpha", 1), # Exact match
("project", 2), # Partial match (case-insensitive)
("Guide", 2), # Word match within a title
("Guide", 1), # Word match within a title
("Special", 0), # No match (nonexistent keyword)
("2024", 2), # Match by numeric keyword
("", 6), # Empty string
("velo", 1), # Accent-insensitive match (velo vs vélo)
("bêta", 1), # Accent-insensitive match (bêta vs beta)
("", 5), # Empty string
],
)
def test_api_documents_descendants_filter_title(query, nb_results):
"""Authenticated users should be able to search documents by their unaccented title."""
"""Authenticated users should be able to search documents by their title."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
@@ -73,7 +70,6 @@ def test_api_documents_descendants_filter_title(query, nb_results):
"User Guide",
"Financial Report 2024",
"Annual Review 2024",
"Guide du vélo urbain", # <-- Title with accent for accent-insensitive test
]
for title in titles:
factories.DocumentFactory(title=title, parent=document)
@@ -89,7 +85,4 @@ def test_api_documents_descendants_filter_title(query, nb_results):
# Ensure all results contain the query in their title
for result in results:
assert (
remove_accents(query).lower().strip()
in remove_accents(result["title"]).lower()
)
assert query.lower().strip() in result["title"].lower()

View File

@@ -15,7 +15,6 @@ import requests
from rest_framework.test import APIClient
from core import factories, models
from core.enums import DocumentAttachmentStatus
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@@ -46,7 +45,6 @@ def test_api_documents_media_auth_anonymous_public():
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
@@ -95,15 +93,7 @@ def test_api_documents_media_auth_extensions():
keys = []
for ext in extensions:
filename = f"{uuid4()!s}.{ext:s}"
key = f"{document_id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
keys.append(key)
keys.append(f"{document_id!s}/attachments/{filename:s}")
factories.DocumentFactory(link_reach="public", attachments=keys)
@@ -152,7 +142,6 @@ def test_api_documents_media_auth_anonymous_attachments():
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
factories.DocumentFactory(id=document_id, link_reach="restricted")
@@ -216,7 +205,6 @@ def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
factories.DocumentFactory(id=document_id, link_reach=reach, attachments=[key])
@@ -295,7 +283,6 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
document = factories.DocumentFactory(
@@ -334,70 +321,3 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
timeout=1,
)
assert response.content.decode("utf-8") == "my prose"
def test_api_documents_media_auth_not_ready_status():
"""Attachments with status not ready should not be accessible"""
document_id = uuid4()
filename = f"{uuid4()!s}.jpg"
key = f"{document_id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.PROCESSING},
)
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
original_url = f"http://localhost/media/{key:s}"
response = APIClient().get(
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
)
assert response.status_code == 403
def test_api_documents_media_auth_missing_status_metadata():
"""Attachments without status metadata should be considered as ready"""
document_id = uuid4()
filename = f"{uuid4()!s}.jpg"
key = f"{document_id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
)
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
original_url = f"http://localhost/media/{key:s}"
response = APIClient().get(
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
)
assert response.status_code == 200
authorization = response["Authorization"]
assert "AWS4-HMAC-SHA256 Credential=" in authorization
assert (
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
in authorization
)
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
response = requests.get(
file_url,
headers={
"authorization": authorization,
"x-amz-date": response["x-amz-date"],
"x-amz-content-sha256": response["x-amz-content-sha256"],
"Host": f"{s3_url.hostname:s}:{s3_url.port:d}",
},
timeout=1,
)
assert response.content.decode("utf-8") == "my prose"

View File

@@ -1,244 +0,0 @@
"""Test the "media_check" endpoint."""
from io import BytesIO
from uuid import uuid4
from django.core.files.storage import default_storage
import pytest
from rest_framework.test import APIClient
from core import factories
from core.enums import DocumentAttachmentStatus
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
def test_api_documents_media_check_unknown_document():
"""
The "media_check" endpoint should return a 404 error if the document does not exist.
"""
client = APIClient()
response = client.get(f"/api/v1.0/documents/{uuid4()!s}media-check/")
assert response.status_code == 404
def test_api_documents_media_check_missing_key():
"""
The "media_check" endpoint should return a 404 error if the key is missing.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
document = factories.DocumentFactory(users=[user])
response = client.get(f"/api/v1.0/documents/{document.id!s}/media-check/")
assert response.status_code == 400
assert response.json() == {"detail": "Missing 'key' query parameter"}
def test_api_documents_media_check_key_parameter_not_related_to_document():
"""
The "media_check" endpoint should return a 404 error if the key is not related to the document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
document = factories.DocumentFactory(users=[user])
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/",
{"key": f"{document.id!s}/attachments/unknown.jpg"},
)
assert response.status_code == 404
assert response.json() == {"detail": "Attachment missing"}
def test_api_documents_media_check_anonymous_public_document():
"""
The "media_check" endpoint should return a 200 status code if the document is public.
"""
document = factories.DocumentFactory(link_reach="public")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.PROCESSING},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {"status": DocumentAttachmentStatus.PROCESSING}
def test_api_documents_media_check_anonymous_public_document_ready():
"""
The "media_check" endpoint should return a 200 status code if the document is public.
"""
document = factories.DocumentFactory(link_reach="public")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {
"status": DocumentAttachmentStatus.READY,
"file": f"/media/{key:s}",
}
@pytest.mark.parametrize("link_reach", ["restricted", "authenticated"])
def test_api_documents_media_check_anonymous_non_public_document(link_reach):
"""
The "media_check" endpoint should return a 403 error if the document is not public.
"""
document = factories.DocumentFactory(link_reach=link_reach)
client = APIClient()
response = client.get(f"/api/v1.0/documents/{document.id!s}/media-check/")
assert response.status_code == 401
def test_api_documents_media_check_connected_document():
"""
The "media_check" endpoint should return a 200 status code for a user connected
checking for a document with link_reach authenticated.
"""
document = factories.DocumentFactory(link_reach="authenticated")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {
"status": DocumentAttachmentStatus.READY,
"file": f"/media/{key:s}",
}
def test_api_documents_media_check_connected_document_media_not_related():
"""
The "media_check" endpoint should return a 404 error if the key is not related to the document.
"""
document = factories.DocumentFactory(link_reach="authenticated")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 404
assert response.json() == {"detail": "Attachment missing"}
def test_api_documents_media_check_media_missing_on_storage():
"""
The "media_check" endpoint should return a 404 error if the media is missing on storage.
"""
document = factories.DocumentFactory(link_reach="authenticated")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
document.attachments = [key]
document.save(update_fields=["attachments"])
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 404
assert response.json() == {"detail": "Media not found"}
@pytest.mark.parametrize("via", VIA)
def test_api_documents_media_check_restricted_document(via, mock_user_teams):
"""
The "media_check" endpoint should return a 200 status code if the document is restricted and
the user has access to it.
"""
document = factories.DocumentFactory(link_reach="restricted")
filename = f"{uuid4()!s}.jpg"
key = f"{document.id!s}/attachments/{filename:s}"
default_storage.connection.meta.client.put_object(
Bucket=default_storage.bucket_name,
Key=key,
Body=BytesIO(b"my prose"),
ContentType="text/plain",
Metadata={"status": DocumentAttachmentStatus.READY},
)
document.attachments = [key]
document.save(update_fields=["attachments"])
user = factories.UserFactory()
client = APIClient()
client.force_login(user=user)
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/media-check/", {"key": key}
)
assert response.status_code == 200
assert response.json() == {
"status": DocumentAttachmentStatus.READY,
"file": f"/media/{key:s}",
}

View File

@@ -310,7 +310,7 @@ def test_api_documents_move_authenticated_deleted_target_as_child(position):
def test_api_documents_move_authenticated_deleted_target_as_sibling(position):
"""
It should not be possible to move a document as a sibling of a deleted target document
if the user has no rights on its parent.
if the user has no rigths on its parent.
"""
user = factories.UserFactory()
client = APIClient()

View File

@@ -48,7 +48,6 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": document.link_role == "editor",
"restore": False,
@@ -112,7 +111,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
"link_configuration": False,
"link_select_options": models.LinkReachChoices.get_select_options(links),
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": grand_parent.link_role == "editor",
"restore": False,
@@ -212,7 +210,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": document.link_role == "editor",
"restore": False,
@@ -282,9 +279,8 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"invite_owner": False,
"link_configuration": False,
"link_select_options": models.LinkReachChoices.get_select_options(links),
"media_auth": True,
"media_check": True,
"move": False,
"media_auth": True,
"partial_update": grand_parent.link_role == "editor",
"restore": False,
"retrieve": True,
@@ -464,7 +460,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
"link_configuration": access.role in ["administrator", "owner"],
"link_select_options": models.LinkReachChoices.get_select_options(links),
"media_auth": True,
"media_check": True,
"move": access.role in ["administrator", "owner"],
"partial_update": access.role != "reader",
"restore": access.role == "owner",

View File

@@ -91,7 +91,6 @@ def test_api_documents_trashbin_format():
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False, # Can't move a deleted document
"partial_update": True,
"restore": True,

View File

@@ -31,7 +31,7 @@ def get_ydoc_with_mages(image_keys):
def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_queries):
"""
When an anonymous user updates a document, the attachment keys extracted from the
updated content should be added to the list of "attachments" to the document if these
updated content should be added to the list of "attachments" ot the document if these
attachments are already readable by anonymous users.
"""
image_keys = [f"{uuid4()!s}/attachments/{uuid4()!s}.png" for _ in range(4)]
@@ -78,7 +78,7 @@ def test_api_documents_update_new_attachment_keys_authenticated(
):
"""
When an authenticated user updates a document, the attachment keys extracted from the
updated content should be added to the list of "attachments" to the document if these
updated content should be added to the list of "attachments" ot the document if these
attachments are already readable by the editing user.
"""
user = factories.UserFactory()

View File

@@ -2,8 +2,6 @@
Test config API endpoints in the Impress core app.
"""
import json
from django.test import override_settings
import pytest
@@ -18,16 +16,12 @@ pytestmark = pytest.mark.django_db
@override_settings(
AI_FEATURE_ENABLED=False,
COLLABORATION_WS_URL="http://testcollab/",
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
CRISP_WEBSITE_ID="123",
FRONTEND_CSS_URL="http://testcss/",
FRONTEND_THEME="test-theme",
MEDIA_BASE_URL="http://testserver/",
POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"},
SENTRY_DSN="https://sentry.test/123",
THEME_CUSTOMIZATION_FILE_PATH="",
)
@pytest.mark.parametrize("is_authenticated", [False, True])
def test_api_config(is_authenticated):
@@ -42,116 +36,17 @@ def test_api_config(is_authenticated):
assert response.status_code == HTTP_200_OK
assert response.json() == {
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
"CRISP_WEBSITE_ID": "123",
"ENVIRONMENT": "test",
"FRONTEND_CSS_URL": "http://testcss/",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
"FRONTEND_THEME": "test-theme",
"LANGUAGES": [
["en-us", "English"],
["fr-fr", "Français"],
["de-de", "Deutsch"],
["nl-nl", "Nederlands"],
["es-es", "Español"],
],
"LANGUAGE_CODE": "en-us",
"MEDIA_BASE_URL": "http://testserver/",
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
"SENTRY_DSN": "https://sentry.test/123",
"AI_FEATURE_ENABLED": False,
"theme_customization": {},
}
@override_settings(
THEME_CUSTOMIZATION_FILE_PATH="/not/existing/file.json",
)
@pytest.mark.parametrize("is_authenticated", [False, True])
def test_api_config_with_invalid_theme_customization_file(is_authenticated):
"""Anonymous users should be allowed to get the configuration."""
client = APIClient()
if is_authenticated:
user = factories.UserFactory()
client.force_login(user)
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
content = response.json()
assert content["theme_customization"] == {}
@override_settings(
THEME_CUSTOMIZATION_FILE_PATH="/configuration/theme/invalid.json",
)
@pytest.mark.parametrize("is_authenticated", [False, True])
def test_api_config_with_invalid_json_theme_customization_file(is_authenticated, fs):
"""Anonymous users should be allowed to get the configuration."""
fs.create_file(
"/configuration/theme/invalid.json",
contents="invalid json",
)
client = APIClient()
if is_authenticated:
user = factories.UserFactory()
client.force_login(user)
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
content = response.json()
assert content["theme_customization"] == {}
@override_settings(
THEME_CUSTOMIZATION_FILE_PATH="/configuration/theme/default.json",
)
@pytest.mark.parametrize("is_authenticated", [False, True])
def test_api_config_with_theme_customization(is_authenticated, fs):
"""Anonymous users should be allowed to get the configuration."""
fs.create_file(
"/configuration/theme/default.json",
contents=json.dumps(
{
"colors": {
"primary": "#000000",
"secondary": "#000000",
},
}
),
)
client = APIClient()
if is_authenticated:
user = factories.UserFactory()
client.force_login(user)
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
content = response.json()
assert content["theme_customization"] == {
"colors": {
"primary": "#000000",
"secondary": "#000000",
},
}
@pytest.mark.parametrize("is_authenticated", [False, True])
def test_api_config_with_original_theme_customization(is_authenticated, settings):
"""Anonymous users should be allowed to get the configuration."""
client = APIClient()
if is_authenticated:
user = factories.UserFactory()
client.force_login(user)
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
content = response.json()
with open(settings.THEME_CUSTOMIZATION_FILE_PATH, "r", encoding="utf-8") as f:
theme_customization = json.load(f)
assert content["theme_customization"] == theme_customization

View File

@@ -4,8 +4,10 @@ Test throttling on documents for the AI endpoint.
from unittest.mock import patch
from django.core.cache import cache
from django.test import override_settings
import pytest
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
@@ -23,6 +25,12 @@ class DocumentAPIView(APIView):
return Response({"message": "Success"})
@pytest.fixture(autouse=True)
def clear_cache():
"""Fixture to clear the cache before each test."""
cache.clear()
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("time.time")
def test_api_utils_ai_document_rate_throttle_minute_limit(mock_time):

View File

@@ -5,6 +5,7 @@ Test throttling on users for the AI endpoint.
from unittest.mock import patch
from uuid import uuid4
from django.core.cache import cache
from django.test import override_settings
import pytest
@@ -28,6 +29,12 @@ class DocumentAPIView(APIView):
return Response({"message": "Success"})
@pytest.fixture(autouse=True)
def clear_cache():
"""Fixture to clear the cache before each test."""
cache.clear()
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("time.time")
def test_api_utils_ai_user_rate_throttle_minute_limit(mock_time):

View File

@@ -1,76 +0,0 @@
"""Test malware detection callback."""
import random
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
import pytest
from lasuite.malware_detection.enums import ReportStatus
from core.enums import DocumentAttachmentStatus
from core.factories import DocumentFactory
from core.malware_detection import malware_detection_callback
pytestmark = pytest.mark.django_db
@pytest.fixture(name="safe_file")
def fixture_safe_file():
"""Create a safe file."""
file_path = "test.txt"
default_storage.save(file_path, ContentFile("test"))
yield file_path
default_storage.delete(file_path)
@pytest.fixture(name="unsafe_file")
def fixture_unsafe_file():
"""Create an unsafe file."""
file_path = "unsafe.txt"
default_storage.save(file_path, ContentFile("test"))
yield file_path
def test_malware_detection_callback_safe_status(safe_file):
"""Test malware detection callback with safe status."""
document = DocumentFactory(attachments=[safe_file])
malware_detection_callback(
safe_file,
ReportStatus.SAFE,
error_info={},
document_id=document.id,
)
document.refresh_from_db()
assert safe_file in document.attachments
assert default_storage.exists(safe_file)
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
head_resp = s3_client.head_object(Bucket=bucket_name, Key=safe_file)
metadata = head_resp.get("Metadata", {})
assert metadata["status"] == DocumentAttachmentStatus.READY
def test_malware_detection_callback_unsafe_status(unsafe_file):
"""Test malware detection callback with unsafe status."""
document = DocumentFactory(attachments=[unsafe_file])
malware_detection_callback(
unsafe_file,
random.choice(
[status.value for status in ReportStatus if status != ReportStatus.SAFE]
),
error_info={"error": "test", "error_code": 4001},
document_id=document.id,
)
document.refresh_from_db()
assert unsafe_file not in document.attachments
assert not default_storage.exists(unsafe_file)

View File

@@ -165,7 +165,6 @@ def test_models_documents_get_abilities_forbidden(
"favorite": False,
"invite_owner": False,
"media_auth": False,
"media_check": False,
"move": False,
"link_configuration": False,
"link_select_options": {
@@ -232,7 +231,6 @@ def test_models_documents_get_abilities_reader(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": False,
"restore": False,
@@ -295,7 +293,6 @@ def test_models_documents_get_abilities_editor(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": True,
"restore": False,
@@ -347,7 +344,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": True,
"partial_update": True,
"restore": True,
@@ -396,7 +392,6 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": True,
"partial_update": True,
"restore": False,
@@ -448,7 +443,6 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": True,
"restore": False,
@@ -507,7 +501,6 @@ def test_models_documents_get_abilities_reader_user(
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": access_from_link,
"restore": False,
@@ -564,7 +557,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"restricted": ["reader", "editor"],
},
"media_auth": True,
"media_check": True,
"move": False,
"partial_update": False,
"restore": False,
@@ -799,7 +791,7 @@ def test_models_documents__email_invitation__success_fr():
assert (
f"Test Sender2 (sender2@example.com) vous a invité avec le rôle &quot;propriétaire&quot; "
f"sur le document suivant : {document.title}" in email_content
f"sur le document suivant: {document.title}" in email_content
)
assert f"docs/{document.id}/" in email_content

View File

@@ -3,10 +3,10 @@
from django.conf import settings
from django.urls import include, path, re_path
from lasuite.oidc_login.urls import urlpatterns as oidc_urls
from rest_framework.routers import DefaultRouter
from core.api import viewsets
from core.authentication.urls import urlpatterns as oidc_urls
# - Main endpoints
router = DefaultRouter()

View File

@@ -1,5 +0,0 @@
"""Impress package. Import the celery app early to load shared task form dependencies."""
from .celery_app import app as celery_app
__all__ = ["celery_app"]

View File

@@ -11,9 +11,6 @@ os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
install(check_options=True)
# Can not be loaded only after install call.
from django.conf import settings # pylint: disable=wrong-import-position
app = Celery("impress")
# Using a string here means the worker doesn't have to serialize
@@ -23,4 +20,4 @@ app = Celery("impress")
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django apps.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
app.autodiscover_tasks()

View File

@@ -1,129 +0,0 @@
{
"footer": {
"default": {
"logo": {
"src": "/assets/icon-docs.svg",
"width": "54px",
"alt": "Docs Logo",
"withTitle": true
},
"externalLinks": [
{
"label": "Github",
"href": "https://github.com/suitenumerique/docs/"
},
{
"label": "DINUM",
"href": "https://www.numerique.gouv.fr/dinum/"
},
{
"label": "ZenDiS",
"href": "https://zendis.de/"
},
{
"label": "BlockNote.js",
"href": "https://www.blocknotejs.org/"
}
],
"bottomInformation": {
"label": "Unless otherwise stated, all content on this site is under",
"link": {
"label": "licence etalab-2.0",
"href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md"
}
}
},
"en": {
"legalLinks": [
{
"label": "Legal Notice",
"href": "#"
},
{
"label": "Personal data and cookies",
"href": "#"
},
{
"label": "Accessibility",
"href": "#"
}
],
"bottomInformation": {
"label": "Unless otherwise stated, all content on this site is under",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
},
"fr": {
"legalLinks": [
{
"label": "Mentions légales",
"href": "#"
},
{
"label": "Données personnelles et cookies",
"href": "#"
},
{
"label": "Accessibilité",
"href": "#"
}
],
"bottomInformation": {
"label": "Sauf mention contraire, tout le contenu de ce site est sous",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
},
"de": {
"legalLinks": [
{
"label": "Impressum",
"href": "#"
},
{
"label": "Personenbezogene Daten und Cookies",
"href": "#"
},
{
"label": "Barrierefreiheit",
"href": "#"
}
],
"bottomInformation": {
"label": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
},
"nl": {
"legalLinks": [
{
"label": "Wettelijke bepalingen",
"href": "#"
},
{
"label": "Persoonlijke gegevens en cookies",
"href": "#"
},
{
"label": "Toegankelijkheid",
"href": "#"
}
],
"bottomInformation": {
"label": "Tenzij anders vermeld, is alle inhoud van deze site ondergebracht onder",
"link": {
"label": "licence MIT",
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
}
}
}
}
}

View File

@@ -23,7 +23,7 @@ from sentry_sdk.integrations.logging import ignore_logger
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.getenv("DATA_DIR", os.path.join("/", "data"))
DATA_DIR = os.path.join("/", "data")
def get_release():
@@ -239,7 +239,6 @@ class Base(Configuration):
("fr-fr", "Français"),
("de-de", "Deutsch"),
("nl-nl", "Nederlands"),
("es-es", "Español"),
)
)
@@ -317,7 +316,6 @@ class Base(Configuration):
"django.contrib.staticfiles",
# OIDC third party
"mozilla_django_oidc",
"lasuite.malware_detection",
]
# Cache
@@ -334,12 +332,6 @@ class Base(Configuration):
"rest_framework.parsers.JSONParser",
"nested_multipart_parser.drf.DrfNestedParser",
],
"DEFAULT_RENDERER_CLASSES": [
# 🔒️ Disable BrowsableAPIRenderer which provides forms allowing a user to
# see all the data in the database (ie a serializer with a ForeignKey field
# will generate a form with a field with all possible values of the FK).
"rest_framework.renderers.JSONRenderer",
],
"EXCEPTION_HANDLER": "core.api.exception_handler",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
@@ -413,36 +405,11 @@ class Base(Configuration):
COLLABORATION_WS_URL = values.Value(
None, environ_name="COLLABORATION_WS_URL", environ_prefix=None
)
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = values.BooleanValue(
False,
environ_name="COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
environ_prefix=None,
)
# Frontend
FRONTEND_THEME = values.Value(
None, environ_name="FRONTEND_THEME", environ_prefix=None
)
FRONTEND_HOMEPAGE_FEATURE_ENABLED = values.BooleanValue(
default=True,
environ_name="FRONTEND_HOMEPAGE_FEATURE_ENABLED",
environ_prefix=None,
)
FRONTEND_CSS_URL = values.Value(
None, environ_name="FRONTEND_CSS_URL", environ_prefix=None
)
THEME_CUSTOMIZATION_FILE_PATH = values.Value(
os.path.join(BASE_DIR, "impress/configuration/theme/default.json"),
environ_name="THEME_CUSTOMIZATION_FILE_PATH",
environ_prefix=None,
)
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value(
60 * 60 * 24,
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
environ_prefix=None,
)
# Posthog
POSTHOG_KEY = values.DictValue(
@@ -467,9 +434,7 @@ class Base(Configuration):
# Session
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
)
SESSION_COOKIE_AGE = 60 * 60 * 12
# OIDC - Authorization Code Flow
OIDC_CREATE_USER = values.BooleanValue(
@@ -534,28 +499,6 @@ class Base(Configuration):
environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION",
environ_prefix=None,
)
OIDC_USE_PKCE = values.BooleanValue(
default=False, environ_name="OIDC_USE_PKCE", environ_prefix=None
)
OIDC_PKCE_CODE_CHALLENGE_METHOD = values.Value(
default="S256",
environ_name="OIDC_PKCE_CODE_CHALLENGE_METHOD",
environ_prefix=None,
)
OIDC_PKCE_CODE_VERIFIER_SIZE = values.IntegerValue(
default=64, environ_name="OIDC_PKCE_CODE_VERIFIER_SIZE", environ_prefix=None
)
OIDC_STORE_ACCESS_TOKEN = values.BooleanValue(
default=False, environ_name="OIDC_STORE_ACCESS_TOKEN", environ_prefix=None
)
OIDC_STORE_REFRESH_TOKEN = values.BooleanValue(
default=False, environ_name="OIDC_STORE_REFRESH_TOKEN", environ_prefix=None
)
OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
default=None,
environ_name="OIDC_STORE_REFRESH_TOKEN_KEY",
environ_prefix=None,
)
# WARNING: Enabling this setting allows multiple user accounts to share the same email
# address. This may cause security issues and is not recommended for production use when
@@ -569,23 +512,14 @@ class Base(Configuration):
USER_OIDC_ESSENTIAL_CLAIMS = values.ListValue(
default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None
)
OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue(
default=values.ListValue( # retrocompatibility
default=["first_name", "last_name"],
environ_name="USER_OIDC_FIELDS_TO_FULLNAME",
environ_prefix=None,
),
environ_name="OIDC_USERINFO_FULLNAME_FIELDS",
USER_OIDC_FIELDS_TO_FULLNAME = values.ListValue(
default=["first_name", "last_name"],
environ_name="USER_OIDC_FIELDS_TO_FULLNAME",
environ_prefix=None,
)
OIDC_USERINFO_SHORTNAME_FIELD = values.Value(
default=values.Value( # retrocompatibility
default="first_name",
environ_name="USER_OIDC_FIELD_TO_SHORTNAME",
environ_prefix=None,
),
environ_name="OIDC_USERINFO_SHORTNAME_FIELD",
USER_OIDC_FIELD_TO_SHORTNAME = values.Value(
default="first_name",
environ_name="USER_OIDC_FIELD_TO_SHORTNAME",
environ_prefix=None,
)
@@ -594,9 +528,6 @@ class Base(Configuration):
)
# AI service
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
AI_API_KEY = values.Value(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
@@ -684,33 +615,9 @@ class Base(Configuration):
),
"propagate": False,
},
"docs.security": {
"handlers": ["console"],
"level": values.Value(
"INFO",
environ_name="LOGGING_LEVEL_LOGGERS_SECURITY",
environ_prefix=None,
),
"propagate": False,
},
},
}
MALWARE_DETECTION = {
"BACKEND": values.Value(
"lasuite.malware_detection.backends.dummy.DummyBackend",
environ_name="MALWARE_DETECTION_BACKEND",
environ_prefix=None,
),
"PARAMETERS": values.DictValue(
default={
"callback_path": "core.malware_detection.malware_detection_callback",
},
environ_name="MALWARE_DETECTION_PARAMETERS",
environ_prefix=None,
),
}
API_USERS_LIST_LIMIT = values.PositiveIntegerValue(
default=5,
environ_name="API_USERS_LIST_LIMIT",
@@ -929,11 +836,6 @@ class Production(Base):
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"KEY_PREFIX": values.Value(
"docs",
environ_name="CACHES_KEY_PREFIX",
environ_prefix=None,
),
},
}

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=5; plural=(n%10==1 && (n%100!=11 || n%100!=71 || n%100!=91) ? 0 : n%10==2 && (n%100!=12 || n%100!=72 || n%100!=92) ? 1 : ((n%10>=3 && n%10<=4) || n%10==9) && ((n%100 < 10 || n%100 > 19) || (n%100 < 70 || n%100 > 79) || (n%100 < 90 || n%100 > 99)) ? 2 : (n!=0 && n%1;\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: br-FR\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 "Titouroù personel"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Aotreoù"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Deiziadoù a-bouez"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Gwezennadur"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Titl"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Me eo an aozer"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
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:450 core/api/serializers.py:450
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:586 core/api/serializers.py:586
msgid "Body"
msgstr "Korf"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr "Doare korf"
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Bugel kentañ"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Bugel diwezhañ"
#: 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 "Kleiz"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Dehoù"
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr "Lenner"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr "Merour"
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr "Perc'henn"
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr "Publik"
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr "id"
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr "krouet d'ar/al"
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr "hizivaet d'ar/al"
#: build/lib/core/models.py:168 core/models.py:168
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
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:217 core/models.py:217
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:223 core/models.py:223
msgid "sub"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
msgid "full name"
msgstr "anv klok"
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr "anv berr"
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr "yezh"
#: build/lib/core/models.py:250 core/models.py:250
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:258 core/models.py:258
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:261 core/models.py:261
msgid "device"
msgstr "trevnad"
#: build/lib/core/models.py:263 core/models.py:263
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:266 core/models.py:266
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:268 core/models.py:268
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:271 core/models.py:271
msgid "active"
msgstr ""
#: build/lib/core/models.py:274 core/models.py:274
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:286 core/models.py:286
msgid "user"
msgstr "implijer"
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "titl"
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr ""
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1299 core/models.py:1299
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Digeriñ"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""

View File

@@ -1,399 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-04 13:46+0000\n"
"PO-Revision-Date: 2025-04-16 16:32\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: zh-CN\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:16 core/api/filters.py:16
msgid "Title"
msgstr "标题"
#: build/lib/core/api/filters.py:30 core/api/filters.py:30
msgid "Creator is me"
msgstr "创建者是我"
#: build/lib/core/api/filters.py:33 core/api/filters.py:33
msgid "Favorite"
msgstr "收藏"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
msgid "A new document was created on your behalf!"
msgstr "已为您创建了一份新文档!"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
msgid "You have been granted ownership of a new document:"
msgstr "您已被授予新文档的所有权:"
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:944 core/api/viewsets.py:944
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr "响应格式无效或令牌验证失败"
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr "用户账户已被禁用"
#: build/lib/core/enums.py:35 core/enums.py:35
msgid "First child"
msgstr "第一个子项"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "Last child"
msgstr "最后一个子项"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "First sibling"
msgstr "第一个同级项"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "Last sibling"
msgstr "最后一个同级项"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Left"
msgstr "左"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Right"
msgstr "右"
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr "阅读者"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr "编辑者"
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr "超级管理员"
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr "所有者"
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr "受限的"
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr "已验证"
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr "公开"
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr "id"
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr "记录的主密钥为 UUID"
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr "创建时间"
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr "记录的创建日期和时间"
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr "更新时间"
#: build/lib/core/models.py:168 core/models.py:168
msgid "date and time at which a record was last updated"
msgstr "记录的最后更新时间"
#: build/lib/core/models.py:204 core/models.py:204
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:217 core/models.py:217
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "请输入有效的 sub。该值只能包含字母、数字及 @/./+/-/_/: 字符。"
#: build/lib/core/models.py:223 core/models.py:223
msgid "sub"
msgstr "sub"
#: build/lib/core/models.py:225 core/models.py:225
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "必填。最多 255 个字符,仅允许字母、数字及 @/./+/-/_/: 字符。"
#: build/lib/core/models.py:234 core/models.py:234
msgid "full name"
msgstr "全名"
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr "简称"
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr "身份电子邮件地址"
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr "管理员电子邮件地址"
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr "语言"
#: build/lib/core/models.py:250 core/models.py:250
msgid "The language in which the user wants to see the interface."
msgstr "用户希望看到的界面语言。"
#: build/lib/core/models.py:258 core/models.py:258
msgid "The timezone in which the user wants to see times."
msgstr "用户查看时间希望的时区。"
#: build/lib/core/models.py:261 core/models.py:261
msgid "device"
msgstr "设备"
#: build/lib/core/models.py:263 core/models.py:263
msgid "Whether the user is a device or a real user."
msgstr "用户是设备还是真实用户。"
#: build/lib/core/models.py:266 core/models.py:266
msgid "staff status"
msgstr "员工状态"
#: build/lib/core/models.py:268 core/models.py:268
msgid "Whether the user can log into this admin site."
msgstr "用户是否可以登录该管理员站点。"
#: build/lib/core/models.py:271 core/models.py:271
msgid "active"
msgstr "激活"
#: build/lib/core/models.py:274 core/models.py:274
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
#: build/lib/core/models.py:286 core/models.py:286
msgid "user"
msgstr "用户"
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
#: core/models.py:470 core/models.py:1154
msgid "title"
msgstr "标题"
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr "文档"
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
#: core/models.py:872
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:907 core/models.py:907
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:911 core/models.py:911
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:917 core/models.py:917
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:1015 core/models.py:1015
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:1045 core/models.py:1045
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1052 core/models.py:1052
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1074 core/models.py:1074
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1081 core/models.py:1081
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1087 core/models.py:1087
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
#: core/models.py:1093 core/models.py:1241
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1155 core/models.py:1155
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1159 core/models.py:1159
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1161 core/models.py:1161
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1167 core/models.py:1167
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1222 core/models.py:1222
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1229 core/models.py:1229
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1235 core/models.py:1235
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1258 core/models.py:1258
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1277 core/models.py:1277
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1298 core/models.py:1298
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "徽标邮件"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "打开"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.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/invitation.html:233
#: core/templates/mail/text/invitation.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-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"POT-Creation-Date: 2025-03-13 11:41+0000\n"
"PO-Revision-Date: 2025-03-17 13:58\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -34,199 +34,204 @@ msgstr "Wichtige Daten"
msgid "Tree structure"
msgstr "Baumstruktur"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:30 core/api/filters.py:30
msgid "Creator is me"
msgstr "Ersteller bin ich"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:33 core/api/filters.py:33
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
#: build/lib/core/api/serializers.py:354 core/api/serializers.py:354
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
#: build/lib/core/api/serializers.py:358 core/api/serializers.py:358
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#: build/lib/core/api/serializers.py:473 core/api/serializers.py:473
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
#: build/lib/core/api/serializers.py:476 core/api/serializers.py:476
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#: build/lib/core/api/serializers.py:482 core/api/serializers.py:482
msgid "Format"
msgstr "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr "Ungültiges Antwortformat oder Token-Verifizierung fehlgeschlagen"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr "Benutzerkonto ist deaktiviert"
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr "Erstes Unterelement"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr "Letztes Unterelement"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr "Erstes Nebenelement"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr "Letztes Nebenelement"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr "Links"
#: build/lib/core/enums.py:41 core/enums.py:41
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Reader"
msgstr "Lesen"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Editor"
msgstr "Bearbeiten"
#: build/lib/core/models.py:65 core/models.py:65
#: build/lib/core/models.py:64 core/models.py:64
msgid "Administrator"
msgstr "Administrator"
#: build/lib/core/models.py:66 core/models.py:66
#: build/lib/core/models.py:65 core/models.py:65
msgid "Owner"
msgstr "Besitzer"
#: build/lib/core/models.py:77 core/models.py:77
#: build/lib/core/models.py:76 core/models.py:76
msgid "Restricted"
msgstr "Beschränkt"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "Authenticated"
msgstr "Authentifiziert"
#: build/lib/core/models.py:83 core/models.py:83
#: build/lib/core/models.py:82 core/models.py:82
msgid "Public"
msgstr "Öffentlich"
#: build/lib/core/models.py:154 core/models.py:154
#: build/lib/core/models.py:153 core/models.py:153
msgid "id"
msgstr "id"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:154 core/models.py:154
msgid "primary key for the record as UUID"
msgstr "primärer Schlüssel für den Datensatz als UUID"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "created on"
msgstr "Erstellt"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:161 core/models.py:161
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:167 core/models.py:167
#: build/lib/core/models.py:166 core/models.py:166
msgid "updated on"
msgstr "Aktualisiert"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
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:204 core/models.py:204
#: build/lib/core/models.py:203 core/models.py:203
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:217 core/models.py:217
#: build/lib/core/models.py:216 core/models.py:216
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:223 core/models.py:223
#: build/lib/core/models.py:222 core/models.py:222
msgid "sub"
msgstr "unter"
#: build/lib/core/models.py:225 core/models.py:225
#: build/lib/core/models.py:224 core/models.py:224
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:234 core/models.py:234
#: build/lib/core/models.py:233 core/models.py:233
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:235 core/models.py:235
#: build/lib/core/models.py:234 core/models.py:234
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:237 core/models.py:237
#: build/lib/core/models.py:236 core/models.py:236
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:241 core/models.py:241
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:249 core/models.py:249
#: build/lib/core/models.py:248 core/models.py:248
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:250 core/models.py:250
#: build/lib/core/models.py:249 core/models.py:249
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:258 core/models.py:258
#: build/lib/core/models.py:257 core/models.py:257
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:261 core/models.py:261
#: build/lib/core/models.py:260 core/models.py:260
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:263 core/models.py:263
#: build/lib/core/models.py:262 core/models.py:262
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:266 core/models.py:266
#: build/lib/core/models.py:265 core/models.py:265
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:268 core/models.py:268
#: build/lib/core/models.py:267 core/models.py:267
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:271 core/models.py:271
#: build/lib/core/models.py:270 core/models.py:270
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:274 core/models.py:274
#: build/lib/core/models.py:273 core/models.py:273
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:286 core/models.py:286
#: build/lib/core/models.py:285 core/models.py:285
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:287 core/models.py:287
#: build/lib/core/models.py:286 core/models.py:286
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
#: build/lib/core/models.py:470 build/lib/core/models.py:1074
#: core/models.py:470 core/models.py:1074
msgid "title"
msgstr "Titel"
@@ -234,136 +239,136 @@ msgstr "Titel"
msgid "excerpt"
msgstr "Auszug"
#: build/lib/core/models.py:519 core/models.py:519
#: build/lib/core/models.py:504 core/models.py:504
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:520 core/models.py:520
#: build/lib/core/models.py:505 core/models.py:505
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
#: build/lib/core/models.py:517 build/lib/core/models.py:826 core/models.py:517
#: core/models.py:826
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:908 core/models.py:908
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:865 core/models.py:865
#, 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:918 core/models.py:918
#: build/lib/core/models.py:871 core/models.py:871
#, 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:1016 core/models.py:1016
#: build/lib/core/models.py:969 core/models.py:969
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1017 core/models.py:1017
#: build/lib/core/models.py:970 core/models.py:970
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:976 core/models.py:976
msgid "A link trace already exists for this document/user."
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:999 core/models.py:999
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1006 core/models.py:1006
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:1075 core/models.py:1075
#: build/lib/core/models.py:1028 core/models.py:1028
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1076 core/models.py:1076
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1082 core/models.py:1082
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1088 core/models.py:1088
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
#: build/lib/core/models.py:1047 build/lib/core/models.py:1161
#: core/models.py:1047 core/models.py:1161
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:1156 core/models.py:1156
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1158 core/models.py:1158
#: build/lib/core/models.py:1077 core/models.py:1077
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1160 core/models.py:1160
#: build/lib/core/models.py:1079 core/models.py:1079
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1162 core/models.py:1162
#: build/lib/core/models.py:1081 core/models.py:1081
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1087 core/models.py:1087
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1169 core/models.py:1169
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1142 core/models.py:1142
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1224 core/models.py:1224
#: build/lib/core/models.py:1143 core/models.py:1143
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1230 core/models.py:1230
#: build/lib/core/models.py:1149 core/models.py:1149
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1236 core/models.py:1236
#: build/lib/core/models.py:1155 core/models.py:1155
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1178 core/models.py:1178
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1299 core/models.py:1299
#: build/lib/core/models.py:1218 core/models.py:1218
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"POT-Creation-Date: 2025-03-13 11:41+0000\n"
"PO-Revision-Date: 2025-03-17 13:58\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -34,199 +34,204 @@ msgstr ""
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Title"
msgstr ""
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:30 core/api/filters.py:30
msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:33 core/api/filters.py:33
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
#: build/lib/core/api/serializers.py:354 core/api/serializers.py:354
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
#: build/lib/core/api/serializers.py:358 core/api/serializers.py:358
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#: build/lib/core/api/serializers.py:473 core/api/serializers.py:473
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
#: build/lib/core/api/serializers.py:476 core/api/serializers.py:476
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#: build/lib/core/api/serializers.py:482 core/api/serializers.py:482
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr ""
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:41 core/enums.py:41
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:65 core/models.py:65
#: build/lib/core/models.py:64 core/models.py:64
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:66 core/models.py:66
#: build/lib/core/models.py:65 core/models.py:65
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:77 core/models.py:77
#: build/lib/core/models.py:76 core/models.py:76
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:83 core/models.py:83
#: build/lib/core/models.py:82 core/models.py:82
msgid "Public"
msgstr ""
#: build/lib/core/models.py:154 core/models.py:154
#: build/lib/core/models.py:153 core/models.py:153
msgid "id"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:154 core/models.py:154
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "created on"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:161 core/models.py:161
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:166 core/models.py:166
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:203 core/models.py:203
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:217 core/models.py:217
#: build/lib/core/models.py:216 core/models.py:216
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:223 core/models.py:223
#: build/lib/core/models.py:222 core/models.py:222
msgid "sub"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
#: build/lib/core/models.py:224 core/models.py:224
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
#: build/lib/core/models.py:233 core/models.py:233
msgid "full name"
msgstr ""
#: build/lib/core/models.py:235 core/models.py:235
#: build/lib/core/models.py:234 core/models.py:234
msgid "short name"
msgstr ""
#: build/lib/core/models.py:237 core/models.py:237
#: build/lib/core/models.py:236 core/models.py:236
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:241 core/models.py:241
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:249 core/models.py:249
#: build/lib/core/models.py:248 core/models.py:248
msgid "language"
msgstr ""
#: build/lib/core/models.py:250 core/models.py:250
#: build/lib/core/models.py:249 core/models.py:249
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:258 core/models.py:258
#: build/lib/core/models.py:257 core/models.py:257
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:261 core/models.py:261
#: build/lib/core/models.py:260 core/models.py:260
msgid "device"
msgstr ""
#: build/lib/core/models.py:263 core/models.py:263
#: build/lib/core/models.py:262 core/models.py:262
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:266 core/models.py:266
#: build/lib/core/models.py:265 core/models.py:265
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:268 core/models.py:268
#: build/lib/core/models.py:267 core/models.py:267
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:271 core/models.py:271
#: build/lib/core/models.py:270 core/models.py:270
msgid "active"
msgstr ""
#: build/lib/core/models.py:274 core/models.py:274
#: build/lib/core/models.py:273 core/models.py:273
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:286 core/models.py:286
#: build/lib/core/models.py:285 core/models.py:285
msgid "user"
msgstr ""
#: build/lib/core/models.py:287 core/models.py:287
#: build/lib/core/models.py:286 core/models.py:286
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
#: build/lib/core/models.py:470 build/lib/core/models.py:1074
#: core/models.py:470 core/models.py:1074
msgid "title"
msgstr ""
@@ -234,136 +239,136 @@ msgstr ""
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:519 core/models.py:519
#: build/lib/core/models.py:504 core/models.py:504
msgid "Document"
msgstr ""
#: build/lib/core/models.py:520 core/models.py:520
#: build/lib/core/models.py:505 core/models.py:505
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
#: build/lib/core/models.py:517 build/lib/core/models.py:826 core/models.py:517
#: core/models.py:826
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:908 core/models.py:908
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#: build/lib/core/models.py:871 core/models.py:871
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:969 core/models.py:969
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1017 core/models.py:1017
#: build/lib/core/models.py:970 core/models.py:970
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:976 core/models.py:976
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:999 core/models.py:999
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
#: build/lib/core/models.py:1028 core/models.py:1028
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1076 core/models.py:1076
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1082 core/models.py:1082
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1088 core/models.py:1088
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
#: build/lib/core/models.py:1047 build/lib/core/models.py:1161
#: core/models.py:1047 core/models.py:1161
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "description"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "code"
msgstr ""
#: build/lib/core/models.py:1158 core/models.py:1158
#: build/lib/core/models.py:1077 core/models.py:1077
msgid "css"
msgstr ""
#: build/lib/core/models.py:1160 core/models.py:1160
#: build/lib/core/models.py:1079 core/models.py:1079
msgid "public"
msgstr ""
#: build/lib/core/models.py:1162 core/models.py:1162
#: build/lib/core/models.py:1081 core/models.py:1081
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
#: build/lib/core/models.py:1087 core/models.py:1087
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1169 core/models.py:1169
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1142 core/models.py:1142
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1224 core/models.py:1224
#: build/lib/core/models.py:1143 core/models.py:1143
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1230 core/models.py:1230
#: build/lib/core/models.py:1149 core/models.py:1149
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1236 core/models.py:1236
#: build/lib/core/models.py:1155 core/models.py:1155
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1178 core/models.py:1178
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1299 core/models.py:1299
#: build/lib/core/models.py:1218 core/models.py:1218
msgid "This email is already associated to a registered user."
msgstr ""

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: es-ES\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 "Información Personal"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Permisos"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Fechas importantes"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Estructura en árbol"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Título"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Yo soy el creador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
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:586 core/api/serializers.py:586
msgid "Body"
msgstr "Cuerpo"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr "Tipo de Cuerpo"
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Primer nodo"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Último nodo"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Primera relación"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Última relación"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Izquierda"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Derecha"
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr "Lector"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr "Editor"
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr "Administrador"
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr "Propietario"
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr "Restringido"
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr "Autentificado"
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr "Público"
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr "id"
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr "clave primaria para el registro como UUID"
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr "creado el"
#: build/lib/core/models.py:162 core/models.py:162
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:167 core/models.py:167
msgid "updated on"
msgstr "actualizado el"
#: build/lib/core/models.py:168 core/models.py:168
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:204 core/models.py:204
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:217 core/models.py:217
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:223 core/models.py:223
msgid "sub"
msgstr "sub (UUID)"
#: build/lib/core/models.py:225 core/models.py:225
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:234 core/models.py:234
msgid "full name"
msgstr "nombre completo"
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr "nombre abreviado"
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr "correo electrónico de identidad"
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr "correo electrónico del administrador"
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr "idioma"
#: build/lib/core/models.py:250 core/models.py:250
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:258 core/models.py:258
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:261 core/models.py:261
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:263 core/models.py:263
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:266 core/models.py:266
msgid "staff status"
msgstr "rol en el equipo"
#: build/lib/core/models.py:268 core/models.py:268
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:271 core/models.py:271
msgid "active"
msgstr "activo"
#: build/lib/core/models.py:274 core/models.py:274
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:286 core/models.py:286
msgid "user"
msgstr "usuario"
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "título"
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr "resumen"
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:912 core/models.py:912
#, 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:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:1023 core/models.py:1023
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:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1053 core/models.py:1053
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:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1156 core/models.py:1156
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1162 core/models.py:1162
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:1168 core/models.py:1168
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1299 core/models.py:1299
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/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "Logo de correo electrónico"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Abrir"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.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/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Presentado por %(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-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"POT-Creation-Date: 2025-03-13 11:41+0000\n"
"PO-Revision-Date: 2025-03-17 13:58\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -24,7 +24,7 @@ msgstr "Infos Personnelles"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Permissions"
msgstr ""
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
@@ -32,340 +32,345 @@ msgstr "Dates importantes"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Arborescence"
msgstr ""
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Title"
msgstr "Titre"
msgstr ""
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:30 core/api/filters.py:30
msgid "Creator is me"
msgstr "Je suis l'auteur"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:33 core/api/filters.py:33
msgid "Favorite"
msgstr "Favoris"
msgstr ""
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
#: build/lib/core/api/serializers.py:354 core/api/serializers.py:354
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
#: build/lib/core/api/serializers.py:358 core/api/serializers.py:358
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:586 core/api/serializers.py:586
#: build/lib/core/api/serializers.py:473 core/api/serializers.py:473
msgid "Body"
msgstr "Corps"
msgstr ""
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
#: build/lib/core/api/serializers.py:476 core/api/serializers.py:476
msgid "Body type"
msgstr "Type de corps"
msgstr ""
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#: build/lib/core/api/serializers.py:482 core/api/serializers.py:482
msgid "Format"
msgstr "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr ""
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr "Premier enfant"
msgstr ""
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr "Dernier enfant"
msgstr ""
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr "Premier frère ou sœur"
msgstr ""
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr "Dernière relation"
msgstr ""
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr "Gauche"
msgstr ""
#: build/lib/core/enums.py:41 core/enums.py:41
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr "Droite"
msgstr ""
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Reader"
msgstr "Lecteur"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Editor"
msgstr "Éditeur"
#: build/lib/core/models.py:65 core/models.py:65
#: build/lib/core/models.py:64 core/models.py:64
msgid "Administrator"
msgstr "Administrateur"
#: build/lib/core/models.py:66 core/models.py:66
#: build/lib/core/models.py:65 core/models.py:65
msgid "Owner"
msgstr "Propriétaire"
#: build/lib/core/models.py:77 core/models.py:77
#: build/lib/core/models.py:76 core/models.py:76
msgid "Restricted"
msgstr "Restreint"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "Authenticated"
msgstr "Authentifié"
#: build/lib/core/models.py:83 core/models.py:83
#: build/lib/core/models.py:82 core/models.py:82
msgid "Public"
msgstr "Public"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
msgid "id"
msgstr ""
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr "identifiant/id"
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr "clé primaire pour l'enregistrement en tant que UUID"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
msgid "created on"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr "créé le"
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr "date et heure de création de l'enregistrement"
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr "mis à jour le"
#: build/lib/core/models.py:168 core/models.py:168
msgid "date and time at which a record was last updated"
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:203 core/models.py:203
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é."
msgstr ""
#: build/lib/core/models.py:217 core/models.py:217
#: build/lib/core/models.py:216 core/models.py:216
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."
msgstr ""
#: build/lib/core/models.py:223 core/models.py:223
#: build/lib/core/models.py:222 core/models.py:222
msgid "sub"
msgstr "sous-groupe"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
#: build/lib/core/models.py:224 core/models.py:224
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_/: uniquement."
msgstr ""
#: build/lib/core/models.py:233 core/models.py:233
msgid "full name"
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
msgid "full name"
msgstr "nom complet"
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr "nom court"
msgstr ""
#: build/lib/core/models.py:237 core/models.py:237
#: build/lib/core/models.py:236 core/models.py:236
msgid "identity email address"
msgstr "adresse e-mail d'identité"
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:241 core/models.py:241
msgid "admin email address"
msgstr "adresse e-mail de l'administrateur"
msgstr ""
#: build/lib/core/models.py:248 core/models.py:248
msgid "language"
msgstr ""
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr "langue"
#: build/lib/core/models.py:250 core/models.py:250
msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
msgstr ""
#: build/lib/core/models.py:258 core/models.py:258
#: build/lib/core/models.py:257 core/models.py:257
msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
msgstr ""
#: build/lib/core/models.py:261 core/models.py:261
#: build/lib/core/models.py:260 core/models.py:260
msgid "device"
msgstr "appareil"
msgstr ""
#: build/lib/core/models.py:263 core/models.py:263
#: build/lib/core/models.py:262 core/models.py:262
msgid "Whether the user is a device or a real user."
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
msgstr ""
#: build/lib/core/models.py:266 core/models.py:266
#: build/lib/core/models.py:265 core/models.py:265
msgid "staff status"
msgstr "statut d'équipe"
msgstr ""
#: build/lib/core/models.py:268 core/models.py:268
#: build/lib/core/models.py:267 core/models.py:267
msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
msgstr ""
#: build/lib/core/models.py:271 core/models.py:271
#: build/lib/core/models.py:270 core/models.py:270
msgid "active"
msgstr "actif"
msgstr ""
#: build/lib/core/models.py:274 core/models.py:274
#: build/lib/core/models.py:273 core/models.py:273
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."
msgstr ""
#: build/lib/core/models.py:285 core/models.py:285
msgid "user"
msgstr ""
#: build/lib/core/models.py:286 core/models.py:286
msgid "user"
msgstr "utilisateur"
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr "utilisateurs"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
#: build/lib/core/models.py:470 build/lib/core/models.py:1074
#: core/models.py:470 core/models.py:1074
msgid "title"
msgstr "titre"
msgstr ""
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr "extrait"
msgstr ""
#: build/lib/core/models.py:519 core/models.py:519
#: build/lib/core/models.py:504 core/models.py:504
msgid "Document"
msgstr "Document"
msgstr ""
#: build/lib/core/models.py:520 core/models.py:520
#: build/lib/core/models.py:505 core/models.py:505
msgid "Documents"
msgstr "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
#: build/lib/core/models.py:517 build/lib/core/models.py:826 core/models.py:517
#: core/models.py:826
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:908 core/models.py:908
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:865 core/models.py:865
#, 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 :"
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant:"
#: build/lib/core/models.py:918 core/models.py:918
#: build/lib/core/models.py:871 core/models.py:871
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} a partagé un document avec vous : {title}"
msgstr "{name} a partagé un document avec vous: {title}"
#: build/lib/core/models.py:1016 core/models.py:1016
#: build/lib/core/models.py:969 core/models.py:969
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
msgstr ""
#: build/lib/core/models.py:1017 core/models.py:1017
#: build/lib/core/models.py:970 core/models.py:970
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:976 core/models.py:976
msgid "A link trace already exists for this document/user."
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
#: build/lib/core/models.py:999 core/models.py:999
msgid "Document favorite"
msgstr "Document favori"
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "Document favorites"
msgstr "Documents favoris"
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1006 core/models.py:1006
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."
msgstr ""
#: build/lib/core/models.py:1028 core/models.py:1028
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 build/lib/core/models.py:1161
#: core/models.py:1047 core/models.py:1161
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
msgid "description"
msgstr ""
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
msgid "code"
msgstr ""
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1077 core/models.py:1077
msgid "css"
msgstr ""
#: build/lib/core/models.py:1079 core/models.py:1079
msgid "public"
msgstr ""
#: build/lib/core/models.py:1081 core/models.py:1081
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1087 core/models.py:1087
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1156 core/models.py:1156
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1162 core/models.py:1162
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:1168 core/models.py:1168
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Modèles"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1142 core/models.py:1142
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
msgstr ""
#: build/lib/core/models.py:1224 core/models.py:1224
#: build/lib/core/models.py:1143 core/models.py:1143
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
msgstr ""
#: build/lib/core/models.py:1230 core/models.py:1230
#: build/lib/core/models.py:1149 core/models.py:1149
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
msgstr ""
#: build/lib/core/models.py:1236 core/models.py:1236
#: build/lib/core/models.py:1155 core/models.py:1155
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1178 core/models.py:1178
msgid "email address"
msgstr "adresse e-mail"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "Document invitation"
msgstr "Invitation à un document"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "Document invitations"
msgstr "Invitations à un document"
msgstr ""
#: build/lib/core/models.py:1299 core/models.py:1299
#: build/lib/core/models.py:1218 core/models.py:1218
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: it\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 "Informazioni personali"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Permessi"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Date importanti"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Struttura ad albero"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Titolo"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Il creatore sono io"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
#: 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 "Sinistra"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Destra"
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr "Lettore"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr "Editor"
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr "Amministratore"
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr "Proprietario"
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr "Limitato"
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr "Autenticato"
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr "Pubblico"
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr "Id"
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr "chiave primaria per il record come UUID"
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr "creato il"
#: build/lib/core/models.py:162 core/models.py:162
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:167 core/models.py:167
msgid "updated on"
msgstr "aggiornato il"
#: build/lib/core/models.py:168 core/models.py:168
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:204 core/models.py:204
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:217 core/models.py:217
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:223 core/models.py:223
msgid "sub"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
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:234 core/models.py:234
msgid "full name"
msgstr "nome completo"
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr "nome"
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr "indirizzo email di identità"
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr "Indirizzo email dell'amministratore"
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr "lingua"
#: build/lib/core/models.py:250 core/models.py:250
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:258 core/models.py:258
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:261 core/models.py:261
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:263 core/models.py:263
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:266 core/models.py:266
msgid "staff status"
msgstr "stato del personale"
#: build/lib/core/models.py:268 core/models.py:268
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:271 core/models.py:271
msgid "active"
msgstr "attivo"
#: build/lib/core/models.py:274 core/models.py:274
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:286 core/models.py:286
msgid "user"
msgstr "utente"
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "titolo"
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:912 core/models.py:912
#, 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:918 core/models.py:918
#, 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:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1299 core/models.py:1299
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "Logo e-mail"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Apri"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"POT-Creation-Date: 2025-03-13 11:41+0000\n"
"PO-Revision-Date: 2025-03-17 13:58\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -34,199 +34,204 @@ msgstr "Belangrijke datums"
msgid "Tree structure"
msgstr "Document structuur"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:30 core/api/filters.py:30
msgid "Creator is me"
msgstr "Ik ben Eigenaar"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:33 core/api/filters.py:33
msgid "Favorite"
msgstr "Favoriete"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
#: build/lib/core/api/serializers.py:354 core/api/serializers.py:354
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document was gecreëerd voor u!"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
#: build/lib/core/api/serializers.py:358 core/api/serializers.py:358
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document:"
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#: build/lib/core/api/serializers.py:473 core/api/serializers.py:473
msgid "Body"
msgstr "Text"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
#: build/lib/core/api/serializers.py:476 core/api/serializers.py:476
msgid "Body type"
msgstr "Text type"
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
#: build/lib/core/api/serializers.py:482 core/api/serializers.py:482
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr "Invalide response formaat of token verificatie gefaald"
#: build/lib/core/enums.py:36 core/enums.py:36
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr "Gebruikersaccount is buiten gebruik gesteld"
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr "Eerste node"
#: build/lib/core/enums.py:37 core/enums.py:37
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr "Laatste node"
#: build/lib/core/enums.py:38 core/enums.py:38
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr "Eerste naaste"
#: build/lib/core/enums.py:39 core/enums.py:39
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr "Laatste naaste"
#: build/lib/core/enums.py:40 core/enums.py:40
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr "Links"
#: build/lib/core/enums.py:41 core/enums.py:41
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Reader"
msgstr "Lezer"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Editor"
msgstr "Bewerker"
#: build/lib/core/models.py:65 core/models.py:65
#: build/lib/core/models.py:64 core/models.py:64
msgid "Administrator"
msgstr "Administrator"
#: build/lib/core/models.py:66 core/models.py:66
#: build/lib/core/models.py:65 core/models.py:65
msgid "Owner"
msgstr "Eigenaar"
#: build/lib/core/models.py:77 core/models.py:77
#: build/lib/core/models.py:76 core/models.py:76
msgid "Restricted"
msgstr "Niet toegestaan"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "Authenticated"
msgstr "Geauthenticeerd"
#: build/lib/core/models.py:83 core/models.py:83
#: build/lib/core/models.py:82 core/models.py:82
msgid "Public"
msgstr "Publiek"
#: build/lib/core/models.py:154 core/models.py:154
#: build/lib/core/models.py:153 core/models.py:153
msgid "id"
msgstr "id"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:154 core/models.py:154
msgid "primary key for the record as UUID"
msgstr "primaire sleutel voor dossier als UUID"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "created on"
msgstr "gemaakt op"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:161 core/models.py:161
msgid "date and time at which a record was created"
msgstr "datum en tijd wanneer dossier was gecreëerd"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:166 core/models.py:166
msgid "updated on"
msgstr "Laatst gewijzigd op"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
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:204 core/models.py:204
#: build/lib/core/models.py:203 core/models.py:203
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:217 core/models.py:217
#: build/lib/core/models.py:216 core/models.py:216
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:223 core/models.py:223
#: build/lib/core/models.py:222 core/models.py:222
msgid "sub"
msgstr "id"
#: build/lib/core/models.py:225 core/models.py:225
#: build/lib/core/models.py:224 core/models.py:224
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:234 core/models.py:234
#: build/lib/core/models.py:233 core/models.py:233
msgid "full name"
msgstr "volledige naam"
#: build/lib/core/models.py:235 core/models.py:235
#: build/lib/core/models.py:234 core/models.py:234
msgid "short name"
msgstr "gebruikersnaam"
#: build/lib/core/models.py:237 core/models.py:237
#: build/lib/core/models.py:236 core/models.py:236
msgid "identity email address"
msgstr "identiteit email adres"
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:241 core/models.py:241
msgid "admin email address"
msgstr "admin email adres"
#: build/lib/core/models.py:249 core/models.py:249
#: build/lib/core/models.py:248 core/models.py:248
msgid "language"
msgstr "taal"
#: build/lib/core/models.py:250 core/models.py:250
#: build/lib/core/models.py:249 core/models.py:249
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:258 core/models.py:258
#: build/lib/core/models.py:257 core/models.py:257
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:261 core/models.py:261
#: build/lib/core/models.py:260 core/models.py:260
msgid "device"
msgstr "apparaat"
#: build/lib/core/models.py:263 core/models.py:263
#: build/lib/core/models.py:262 core/models.py:262
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:266 core/models.py:266
#: build/lib/core/models.py:265 core/models.py:265
msgid "staff status"
msgstr "beheerder status"
#: build/lib/core/models.py:268 core/models.py:268
#: build/lib/core/models.py:267 core/models.py:267
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:271 core/models.py:271
#: build/lib/core/models.py:270 core/models.py:270
msgid "active"
msgstr "actief"
#: build/lib/core/models.py:274 core/models.py:274
#: build/lib/core/models.py:273 core/models.py:273
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:286 core/models.py:286
#: build/lib/core/models.py:285 core/models.py:285
msgid "user"
msgstr "gebruiker"
#: build/lib/core/models.py:287 core/models.py:287
#: build/lib/core/models.py:286 core/models.py:286
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
#: build/lib/core/models.py:470 build/lib/core/models.py:1074
#: core/models.py:470 core/models.py:1074
msgid "title"
msgstr "titel"
@@ -234,136 +239,136 @@ msgstr "titel"
msgid "excerpt"
msgstr "uittreksel"
#: build/lib/core/models.py:519 core/models.py:519
#: build/lib/core/models.py:504 core/models.py:504
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:520 core/models.py:520
#: build/lib/core/models.py:505 core/models.py:505
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
#: build/lib/core/models.py:517 build/lib/core/models.py:826 core/models.py:517
#: core/models.py:826
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:908 core/models.py:908
#: build/lib/core/models.py:861 core/models.py:861
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:865 core/models.py:865
#, 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:918 core/models.py:918
#: build/lib/core/models.py:871 core/models.py:871
#, 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:1016 core/models.py:1016
#: build/lib/core/models.py:969 core/models.py:969
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:1017 core/models.py:1017
#: build/lib/core/models.py:970 core/models.py:970
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:976 core/models.py:976
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:1046 core/models.py:1046
#: build/lib/core/models.py:999 core/models.py:999
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1006 core/models.py:1006
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:1075 core/models.py:1075
#: build/lib/core/models.py:1028 core/models.py:1028
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1076 core/models.py:1076
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1082 core/models.py:1082
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This user is already in this document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1088 core/models.py:1088
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This team is already in this document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
#: build/lib/core/models.py:1047 build/lib/core/models.py:1161
#: core/models.py:1047 core/models.py:1161
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:1156 core/models.py:1156
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1157 core/models.py:1157
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1158 core/models.py:1158
#: build/lib/core/models.py:1077 core/models.py:1077
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1160 core/models.py:1160
#: build/lib/core/models.py:1079 core/models.py:1079
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1162 core/models.py:1162
#: build/lib/core/models.py:1081 core/models.py:1081
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:1168 core/models.py:1168
#: build/lib/core/models.py:1087 core/models.py:1087
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1169 core/models.py:1169
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1223 core/models.py:1223
#: build/lib/core/models.py:1142 core/models.py:1142
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1224 core/models.py:1224
#: build/lib/core/models.py:1143 core/models.py:1143
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1230 core/models.py:1230
#: build/lib/core/models.py:1149 core/models.py:1149
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1236 core/models.py:1236
#: build/lib/core/models.py:1155 core/models.py:1155
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1178 core/models.py:1178
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1197 core/models.py:1197
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1279 core/models.py:1279
#: build/lib/core/models.py:1198 core/models.py:1198
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1299 core/models.py:1299
#: build/lib/core/models.py:1218 core/models.py:1218
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: pt-PT\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 "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
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:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr ""
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
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:217 core/models.py:217
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:223 core/models.py:223
msgid "sub"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
msgid "full name"
msgstr ""
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr ""
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr ""
#: build/lib/core/models.py:250 core/models.py:250
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:258 core/models.py:258
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:261 core/models.py:261
msgid "device"
msgstr ""
#: build/lib/core/models.py:263 core/models.py:263
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:266 core/models.py:266
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:268 core/models.py:268
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:271 core/models.py:271
msgid "active"
msgstr ""
#: build/lib/core/models.py:274 core/models.py:274
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:286 core/models.py:286
msgid "user"
msgstr ""
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr ""
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr ""
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr ""
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr ""
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1299 core/models.py:1299
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\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%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: sl\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 "Osebni podatki"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Dovoljenja"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Pomembni datumi"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Drevesna struktura"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Naslov"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Ustvaril sem jaz"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
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:450 core/api/serializers.py:450
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:586 core/api/serializers.py:586
msgid "Body"
msgstr "Telo"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr "Vrsta telesa"
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr ""
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Prvi otrok"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Zadnji otrok"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Prvi brat in sestra"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Zadnji brat in sestra"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Levo"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Desno"
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr "Bralec"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr "Urednik"
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr "Skrbnik"
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr "Lastnik"
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr "Omejeno"
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr "Preverjeno"
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr "Javno"
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr "primarni ključ za zapis kot UUID"
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr "ustvarjen na"
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr "datum in čas, ko je bil zapis ustvarjen"
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr "posodobljeno dne"
#: build/lib/core/models.py:168 core/models.py:168
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:204 core/models.py:204
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:217 core/models.py:217
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:223 core/models.py:223
msgid "sub"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
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:234 core/models.py:234
msgid "full name"
msgstr "polno ime"
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr "kratko ime"
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr "elektronski naslov identitete"
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr "elektronski naslov skrbnika"
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr "jezik"
#: build/lib/core/models.py:250 core/models.py:250
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:258 core/models.py:258
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:261 core/models.py:261
msgid "device"
msgstr "naprava"
#: build/lib/core/models.py:263 core/models.py:263
msgid "Whether the user is a device or a real user."
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
#: build/lib/core/models.py:266 core/models.py:266
msgid "staff status"
msgstr "kadrovski status"
#: build/lib/core/models.py:268 core/models.py:268
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:271 core/models.py:271
msgid "active"
msgstr "aktivni"
#: build/lib/core/models.py:274 core/models.py:274
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:286 core/models.py:286
msgid "user"
msgstr "uporabnik"
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "naslov"
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr "odlomek"
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:912 core/models.py:912
#, 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:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1053 core/models.py:1053
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:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
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:1156 core/models.py:1156
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1299 core/models.py:1299
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/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "E-pošta z logotipom"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Odpri"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.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/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Pod okriljem %(brandname)s "

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: sv-SE\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 "Personuppgifter"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Behörigheter"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Viktiga datum"
#: 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 "Titel"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Skaparen är jag"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
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:586 core/api/serializers.py:586
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
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:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr "Administratör"
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr "Publik"
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
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:217 core/models.py:217
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:223 core/models.py:223
msgid "sub"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
msgid "full name"
msgstr ""
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr ""
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr ""
#: build/lib/core/models.py:250 core/models.py:250
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:258 core/models.py:258
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:261 core/models.py:261
msgid "device"
msgstr ""
#: build/lib/core/models.py:263 core/models.py:263
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:266 core/models.py:266
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:268 core/models.py:268
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:271 core/models.py:271
msgid "active"
msgstr "aktiv"
#: build/lib/core/models.py:274 core/models.py:274
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:286 core/models.py:286
msgid "user"
msgstr ""
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr ""
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr ""
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr ""
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr ""
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1299 core/models.py:1299
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/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "Logotyp e-post"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Öppna"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: tr\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 "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
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:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr ""
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
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:217 core/models.py:217
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:223 core/models.py:223
msgid "sub"
msgstr ""
#: build/lib/core/models.py:225 core/models.py:225
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
msgid "full name"
msgstr ""
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr ""
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr ""
#: build/lib/core/models.py:250 core/models.py:250
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:258 core/models.py:258
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:261 core/models.py:261
msgid "device"
msgstr ""
#: build/lib/core/models.py:263 core/models.py:263
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:266 core/models.py:266
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:268 core/models.py:268
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:271 core/models.py:271
msgid "active"
msgstr ""
#: build/lib/core/models.py:274 core/models.py:274
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:286 core/models.py:286
msgid "user"
msgstr ""
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr ""
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr ""
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr ""
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr ""
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr ""
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr ""
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr ""
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1299 core/models.py:1299
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""

View File

@@ -1,390 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
"PO-Revision-Date: 2025-05-22 14:16\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: zh-CN\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 "Favorite"
msgstr "收藏"
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
msgid "A new document was created on your behalf!"
msgstr "已为您创建了一份新文档!"
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
msgid "You have been granted ownership of a new document:"
msgstr "您已被授予新文档的所有权:"
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
#: 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:56 build/lib/core/models.py:63 core/models.py:56
#: core/models.py:63
msgid "Reader"
msgstr "阅读者"
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
#: core/models.py:64
msgid "Editor"
msgstr "编辑者"
#: build/lib/core/models.py:65 core/models.py:65
msgid "Administrator"
msgstr "超级管理员"
#: build/lib/core/models.py:66 core/models.py:66
msgid "Owner"
msgstr "所有者"
#: build/lib/core/models.py:77 core/models.py:77
msgid "Restricted"
msgstr "受限的"
#: build/lib/core/models.py:81 core/models.py:81
msgid "Authenticated"
msgstr "已验证"
#: build/lib/core/models.py:83 core/models.py:83
msgid "Public"
msgstr "公开"
#: build/lib/core/models.py:154 core/models.py:154
msgid "id"
msgstr "id"
#: build/lib/core/models.py:155 core/models.py:155
msgid "primary key for the record as UUID"
msgstr "记录的主密钥为 UUID"
#: build/lib/core/models.py:161 core/models.py:161
msgid "created on"
msgstr "创建时间"
#: build/lib/core/models.py:162 core/models.py:162
msgid "date and time at which a record was created"
msgstr "记录的创建日期和时间"
#: build/lib/core/models.py:167 core/models.py:167
msgid "updated on"
msgstr "更新时间"
#: build/lib/core/models.py:168 core/models.py:168
msgid "date and time at which a record was last updated"
msgstr "记录的最后更新时间"
#: build/lib/core/models.py:204 core/models.py:204
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:217 core/models.py:217
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "请输入有效的 sub。该值只能包含字母、数字及 @/./+/-/_/: 字符。"
#: build/lib/core/models.py:223 core/models.py:223
msgid "sub"
msgstr "sub"
#: build/lib/core/models.py:225 core/models.py:225
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "必填。最多 255 个字符,仅允许字母、数字及 @/./+/-/_/: 字符。"
#: build/lib/core/models.py:234 core/models.py:234
msgid "full name"
msgstr "全名"
#: build/lib/core/models.py:235 core/models.py:235
msgid "short name"
msgstr "简称"
#: build/lib/core/models.py:237 core/models.py:237
msgid "identity email address"
msgstr "身份电子邮件地址"
#: build/lib/core/models.py:242 core/models.py:242
msgid "admin email address"
msgstr "管理员电子邮件地址"
#: build/lib/core/models.py:249 core/models.py:249
msgid "language"
msgstr "语言"
#: build/lib/core/models.py:250 core/models.py:250
msgid "The language in which the user wants to see the interface."
msgstr "用户希望看到的界面语言。"
#: build/lib/core/models.py:258 core/models.py:258
msgid "The timezone in which the user wants to see times."
msgstr "用户查看时间希望的时区。"
#: build/lib/core/models.py:261 core/models.py:261
msgid "device"
msgstr "设备"
#: build/lib/core/models.py:263 core/models.py:263
msgid "Whether the user is a device or a real user."
msgstr "用户是设备还是真实用户。"
#: build/lib/core/models.py:266 core/models.py:266
msgid "staff status"
msgstr "员工状态"
#: build/lib/core/models.py:268 core/models.py:268
msgid "Whether the user can log into this admin site."
msgstr "用户是否可以登录该管理员站点。"
#: build/lib/core/models.py:271 core/models.py:271
msgid "active"
msgstr "激活"
#: build/lib/core/models.py:274 core/models.py:274
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
#: build/lib/core/models.py:286 core/models.py:286
msgid "user"
msgstr "用户"
#: build/lib/core/models.py:287 core/models.py:287
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
#: core/models.py:470 core/models.py:1155
msgid "title"
msgstr "标题"
#: build/lib/core/models.py:471 core/models.py:471
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:519 core/models.py:519
msgid "Document"
msgstr "文档"
#: build/lib/core/models.py:520 core/models.py:520
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
#: core/models.py:873
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:908 core/models.py:908
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:912 core/models.py:912
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:918 core/models.py:918
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:1016 core/models.py:1016
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:1017 core/models.py:1017
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:1046 core/models.py:1046
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1075 core/models.py:1075
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1076 core/models.py:1076
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1082 core/models.py:1082
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1088 core/models.py:1088
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
#: core/models.py:1094 core/models.py:1242
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1156 core/models.py:1156
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1157 core/models.py:1157
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1158 core/models.py:1158
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1160 core/models.py:1160
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1162 core/models.py:1162
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1168 core/models.py:1168
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1169 core/models.py:1169
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1223 core/models.py:1223
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1224 core/models.py:1224
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1230 core/models.py:1230
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1236 core/models.py:1236
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1259 core/models.py:1259
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1278 core/models.py:1278
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1279 core/models.py:1279
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1299 core/models.py:1299
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "徽标邮件"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "打开"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.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/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " 由 %(brandname)s 倾力打造。 "

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.3.0"
version = "3.0.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,40 +25,40 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.13.4",
"boto3==1.38.27",
"beautifulsoup4==4.12.3",
"boto3==1.37.18",
"Brotli==1.1.0",
"celery[redis]==5.5.3",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-filter==25.1",
"django-lasuite[all]==0.0.9",
"django-parler==2.3",
"redis==5.2.1",
"django-redis==5.4.0",
"django-storages[s3]==1.14.6",
"django-storages[s3]==1.14.5",
"django-timezone-field>=5.1",
"django==5.1.9",
"django==5.1.7",
"django-treebeard==4.7.1",
"djangorestframework==3.16.0",
"djangorestframework==3.15.2",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10",
"factory_boy==3.3.3",
"gunicorn==23.0.0",
"jsonschema==4.24.0",
"lxml==5.4.0",
"markdown==3.8",
"jsonschema==4.23.0",
"lxml==5.3.1",
"markdown==3.7",
"mozilla-django-oidc==4.0.1",
"nested-multipart-parser==1.5.0",
"openai==1.82.1",
"psycopg[binary]==3.2.9",
"pycrdt==0.12.20",
"openai==1.68.2",
"psycopg[binary]==3.2.6",
"pycrdt==0.12.10",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"redis<6.0.0",
"requests==2.32.3",
"sentry-sdk==2.29.1",
"sentry-sdk==2.24.0",
"url-normalize==1.4.3",
"whitenoise==6.9.0",
]
@@ -70,23 +70,23 @@ dependencies = [
[project.optional-dependencies]
dev = [
"django-extensions==4.1",
"django-test-migrations==1.5.0",
"drf-spectacular-sidecar==2025.6.1",
"freezegun==1.5.2",
"django-extensions==3.2.3",
"django-test-migrations==1.4.0",
"drf-spectacular-sidecar==2025.3.1",
"freezegun==1.5.1",
"ipdb==0.13.13",
"ipython==9.3.0",
"ipython==9.0.2",
"pyfakefs==5.8.0",
"pylint-django==2.6.1",
"pylint==3.3.7",
"pytest-cov==6.1.1",
"pytest-django==4.11.1",
"pylint==3.3.6",
"pytest-cov==6.0.0",
"pytest-django==4.10.0",
"pytest==8.3.5",
"pytest-icdiff==0.9",
"pytest-xdist==3.7.0",
"pytest-xdist==3.6.1",
"responses==0.25.7",
"ruff==0.11.12",
"types-requests==2.32.0.20250602",
"ruff==0.11.2",
"types-requests==2.32.0.20250306",
]
[tool.setuptools]

View File

@@ -1,9 +1,4 @@
FROM node:24-alpine AS frontend-deps
# Upgrade system packages to install security updates
RUN apk update && \
apk upgrade && \
rm -rf /var/cache/apk/*
FROM node:20-alpine AS frontend-deps
WORKDIR /home/frontend/
@@ -44,13 +39,10 @@ ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
ARG SW_DEACTIVATED
ENV NEXT_PUBLIC_SW_DEACTIVATED=${SW_DEACTIVATED}
ARG PUBLISH_AS_MIT
ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
RUN yarn build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
# Un-privileged user running the application
ARG DOCKER_USER

View File

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

View File

@@ -10,6 +10,8 @@ test.beforeEach(async ({ page }) => {
test.describe('404', () => {
test('Checks all the elements are visible', async ({ page }) => {
await expect(page.getByLabel('Image 404')).toBeVisible();
await expect(page.getByText('Ouch')).toBeVisible();
await expect(
page.getByText(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',

View File

@@ -1,9 +1,4 @@
<svg
width="100"
height="100"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="30" r="20" fill="#3498db" />
<polygon
points="50,10 55,20 65,20 58,30 60,40 50,35 40,40 42,30 35,20 45,20"

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 292 B

View File

@@ -1,53 +1,15 @@
import { Page, expect } from '@playwright/test';
export const CONFIG = {
AI_FEATURE_ENABLED: true,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: false,
ENVIRONMENT: 'development',
FRONTEND_CSS_URL: null,
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
FRONTEND_THEME: null,
MEDIA_BASE_URL: 'http://localhost:8083',
LANGUAGES: [
['en-us', 'English'],
['fr-fr', 'Français'],
['de-de', 'Deutsch'],
['nl-nl', 'Nederlands'],
['es-es', 'Español'],
],
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
SENTRY_DSN: null,
theme_customization: {},
} as const;
export const overrideConfig = async (
page: Page,
newConfig: { [K in keyof typeof CONFIG]?: unknown },
) =>
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
...newConfig,
},
});
} else {
await route.continue();
}
});
export const keyCloakSignIn = async (
page: Page,
browserName: string,
fromHome: boolean = true,
) => {
if (fromHome) {
await page.getByRole('button', { name: 'Start Writing' }).first().click();
await page
.getByRole('button', { name: 'Proconnect Login' })
.first()
.click();
}
const login = `user-e2e-${browserName}`;
@@ -95,7 +57,6 @@ export const createDoc = async (
});
const input = page.getByLabel('doc title input');
await expect(input).toBeVisible();
await expect(input).toHaveText('');
await input.click();
@@ -107,16 +68,9 @@ export const createDoc = async (
};
export const verifyDocName = async (page: Page, docName: string) => {
await expect(
page.getByLabel('It is the card information about the document.'),
).toBeVisible({
timeout: 10000,
});
const input = page.getByRole('textbox', { name: 'doc title input' });
try {
await expect(
page.getByRole('textbox', { name: 'doc title input' }),
).toHaveText(docName);
await expect(input).toHaveText(docName);
} catch {
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
}
@@ -125,7 +79,7 @@ export const verifyDocName = async (page: Page, docName: string) => {
export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
fillText: string = 'user ',
) => {
const responsePromiseSearchUser = page.waitForResponse(

View File

@@ -2,14 +2,55 @@ import path from 'path';
import { expect, test } from '@playwright/test';
import { CONFIG, createDoc, overrideConfig } from './common';
import { createDoc, verifyDocName } from './common';
const config = {
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
ENVIRONMENT: 'development',
FRONTEND_THEME: 'default',
MEDIA_BASE_URL: 'http://localhost:8083',
LANGUAGES: [
['en-us', 'English'],
['fr-fr', 'Français'],
['de-de', 'Deutsch'],
['nl-nl', 'Nederlands'],
],
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
SENTRY_DSN: null,
};
test.describe('Config', () => {
test('it checks the config api is called', async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
);
await page.goto('/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
expect(await response.json()).toStrictEqual(config);
});
test('it checks that sentry is trying to init from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
SENTRY_DSN: 'https://sentry.io/123',
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...config,
SENTRY_DSN: 'https://sentry.io/123',
},
});
} else {
await route.continue();
}
});
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
@@ -55,48 +96,42 @@ test.describe('Config', () => {
test('it checks that collaboration server is configured from config endpoint', async ({
page,
}) => {
await page.goto('/');
void page
.getByRole('button', {
name: 'New doc',
})
.click();
const webSocket = await page.waitForEvent('websocket', (webSocket) => {
return webSocket.url().includes('ws://localhost:4444/collaboration/ws/');
});
expect(webSocket.url()).toContain('ws://localhost:4444/collaboration/ws/');
});
test('it checks the AI feature flag from config endpoint', async ({
page,
browserName,
}) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: false,
const webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
return webSocket.url().includes('ws://localhost:4444/collaboration/ws/');
});
await page.goto('/');
await createDoc(page, 'doc-ai-feature', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
expect(await page.locator('button[data-test="ai-actions"]').count()).toBe(
0,
const randomDoc = await createDoc(
page,
'doc-collaboration',
browserName,
1,
);
await verifyDocName(page, randomDoc[0]);
const webSocket = await webSocketPromise;
expect(webSocket.url()).toContain('ws://localhost:4444/collaboration/ws/');
});
test('it checks that Crisp is trying to init from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
CRISP_WEBSITE_ID: '1234',
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...config,
CRISP_WEBSITE_ID: '1234',
},
});
} else {
await route.continue();
}
});
await page.goto('/');
@@ -105,26 +140,14 @@ test.describe('Config', () => {
page.locator('#crisp-chatbox').getByText('Invalid website'),
).toBeVisible();
});
test('it checks FRONTEND_CSS_URL config', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
});
await page.goto('/');
await expect(
page
.locator('head link[href="http://localhost:123465/css/style.css"]')
.first(),
).toBeAttached();
});
});
test.describe('Config: Not loggued', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('it checks the config api is called', async ({ page }) => {
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
@@ -135,25 +158,11 @@ test.describe('Config: Not loggued', () => {
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const json = (await response.json()) as typeof CONFIG;
const { theme_customization, ...configApi } = json;
expect(theme_customization).toBeDefined();
const { theme_customization: _, ...CONFIG_LEFT } = CONFIG;
const jsonResponse = await response.json();
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('default');
expect(configApi).toStrictEqual(CONFIG_LEFT);
});
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
const header = page.locator('header').first();
const footer = page.locator('footer').first();
// alt 'Gouvernement Logo' comes from the theme
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
});
});

View File

@@ -4,8 +4,6 @@ import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import {
CONFIG,
addNewMember,
createDoc,
goToGridDoc,
mockedDocument,
@@ -27,11 +25,7 @@ test.describe('Doc Editor', () => {
await editor.click();
await editor.fill('test content');
await editor
.getByText('test content', {
exact: true,
})
.selectText();
await editor.getByText('test content').dblclick();
const toolbar = page.locator('.bn-formatting-toolbar');
await expect(toolbar.locator('button[data-test="bold"]')).toBeVisible();
@@ -64,18 +58,18 @@ test.describe('Doc Editor', () => {
* - signal of the backend to the collaborative server (connection should close)
* - reconnection to the collaborative server
*/
test('checks the connection with collaborative server', async ({ page }) => {
test('checks the connection with collaborative server', async ({
page,
browserName,
}) => {
let webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
return webSocket
.url()
.includes('ws://localhost:4444/collaboration/ws/?room=');
});
await page
.getByRole('button', {
name: 'New doc',
})
.click();
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1);
await verifyDocName(page, randomDoc[0]);
let webSocket = await webSocketPromise;
expect(webSocket.url()).toContain(
@@ -105,7 +99,7 @@ test.describe('Doc Editor', () => {
const wsClose = await wsClosePromise;
expect(wsClose.isClosed()).toBeTruthy();
// Check the ws is connected again
// Checkt the ws is connected again
webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
return webSocket
.url()
@@ -132,7 +126,7 @@ test.describe('Doc Editor', () => {
await expect(editor.getByText('[test markdown]')).toBeVisible();
await editor.getByText('[test markdown]').selectText();
await editor.getByText('[test markdown]').dblclick();
await page.locator('button[data-test="convertMarkdown"]').click();
await expect(editor.getByText('[test markdown]')).toBeHidden();
@@ -225,8 +219,11 @@ test.describe('Doc Editor', () => {
await editor.fill('Hello World Doc persisted 2');
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
const urlDoc = page.url();
await page.goto(urlDoc);
await page.goto('/');
await goToGridDoc(page, {
title: doc,
});
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
});
@@ -300,7 +297,7 @@ test.describe('Doc Editor', () => {
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await editor.getByText('Hello').dblclick();
await page.getByRole('button', { name: 'AI' }).click();
@@ -341,7 +338,6 @@ test.describe('Doc Editor', () => {
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
browserName,
}) => {
await mockedDocument(page, {
accesses: [
@@ -365,25 +361,19 @@ test.describe('Doc Editor', () => {
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_reach: 'public',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai',
browserName,
1,
);
await goToGridDoc(page);
await verifyDocName(page, randomDoc);
await verifyDocName(page, 'Mocked document');
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await editor.getByText('Hello').dblclick();
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
@@ -425,10 +415,6 @@ test.describe('Doc Editor', () => {
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`html`);
});
const responseCheckPromise = page.waitForResponse(
(response) =>
response.url().includes('media-check') && response.status() === 200,
);
await verifyDocName(page, randomDoc);
@@ -443,8 +429,6 @@ test.describe('Doc Editor', () => {
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
await responseCheckPromise;
await page.locator('.bn-block-content[data-name="test.html"]').click();
await page.getByRole('button', { name: 'Download file' }).click();
@@ -460,136 +444,4 @@ test.describe('Doc Editor', () => {
const svgBuffer = await cs.toBuffer(await download.createReadStream());
expect(svgBuffer.toString()).toContain('Hello svg');
});
test('it analyzes uploads', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
let requestCount = 0;
await page.route(
/.*\/documents\/.*\/media-check\/\?key=.*/,
async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
status: requestCount ? 'ready' : 'processing',
file: '/anything.html',
},
});
requestCount++;
} else {
await route.continue();
}
},
);
const fileChooserPromise = page.waitForEvent('filechooser');
await verifyDocName(page, randomDoc);
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();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
await expect(editor.getByText('Analyzing file...')).toBeVisible();
// The retry takes a few seconds
await expect(editor.getByText('test.html')).toBeVisible({
timeout: 7000,
});
await expect(editor.getByText('Analyzing file...')).toBeHidden();
});
test('it checks block editing when not connected to collab server', async ({
page,
}) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
COLLABORATION_WS_URL: 'ws://localhost:5555/collaboration/ws/',
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
},
});
} else {
await route.continue();
}
});
await page.goto('/');
void page
.getByRole('button', {
name: 'New doc',
})
.click();
const card = page.getByLabel('It is the card information');
await expect(
card.getByText('Your network do not allow you to edit'),
).toBeHidden();
const editor = page.locator('.ProseMirror');
await expect(editor).toHaveAttribute('contenteditable', 'true');
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Editor', 'impress');
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
await expect(
card.getByText('Your network do not allow you to edit'),
).toBeVisible({
timeout: 10000,
});
await expect(editor).toHaveAttribute('contenteditable', 'false');
});
test('it checks if callout custom block', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
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
.locator('div[data-content-type="callout"]')
.first();
await expect(calloutBlock).toBeVisible();
await calloutBlock.locator('.inline-content').fill('example text');
await expect(page.locator('.bn-block').first()).toHaveAttribute(
'data-background-color',
'yellow',
);
const emojiButton = calloutBlock.getByRole('button');
await expect(emojiButton).toHaveText('💡');
await emojiButton.click();
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();
await page.locator('.bn-color-picker-dropdown > button').last().click();
await expect(page.locator('.bn-block').first()).toHaveAttribute(
'data-background-color',
'pink',
);
});
});

View File

@@ -79,7 +79,7 @@ test.describe('Doc Export', () => {
})
.click();
void page
await page
.getByRole('button', {
name: 'Download',
})
@@ -129,7 +129,7 @@ test.describe('Doc Export', () => {
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
void page
await page
.getByRole('button', {
name: 'Download',
})
@@ -141,7 +141,7 @@ test.describe('Doc Export', () => {
/**
* This test tell us that the export to pdf is working with images
* but it does not tell us if the images are being displayed correctly
* but it does not tell us if the images are beeing displayed correctly
* in the pdf.
*
* TODO: Check if the images are displayed correctly in the pdf
@@ -206,7 +206,7 @@ test.describe('Doc Export', () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
void page
await page
.getByRole('button', {
name: 'Download',
})
@@ -235,7 +235,7 @@ test.describe('Doc Export', () => {
// Trigger slash menu to show menu
await editor.click();
await editor.fill('/');
await page.getByText('Quote or excerpt').click();
await page.getByText('Add a quote block').click();
await expect(
editor.locator('.bn-block-content[data-content-type="quote"]'),
@@ -254,7 +254,7 @@ test.describe('Doc Export', () => {
})
.click();
void page
await page
.getByRole('button', {
name: 'Download',
})
@@ -298,7 +298,7 @@ test.describe('Doc Export', () => {
})
.click();
void page
await page
.getByRole('button', {
name: 'Download',
})

View File

@@ -190,7 +190,7 @@ test.describe('Document grid item options', () => {
test.describe('Documents filters', () => {
test('it checks the prebuild left panel filters', async ({ page }) => {
void page.goto('/');
await page.goto('/');
// All Docs
const response = await page.waitForResponse(
@@ -208,6 +208,7 @@ test.describe('Documents filters', () => {
// Initial state
await expect(allDocs).toBeVisible();
await expect(allDocs).toHaveCSS('background-color', 'rgb(238, 238, 238)');
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
await expect(myDocs).toBeVisible();
@@ -262,7 +263,7 @@ test.describe('Documents filters', () => {
test.describe('Documents Grid', () => {
test('checks all the elements are visible', async ({ page }) => {
void page.goto('/');
await page.goto('/');
let docs: SmallDoc[] = [];
const response = await page.waitForResponse(

View File

@@ -152,7 +152,12 @@ test.describe('Document list members', () => {
await expect(soloOwner).toBeHidden();
await list.click();
const otherOwner = page.getByText(
`You cannot update the role or remove other owner.`,
);
await newUserRoles.click();
await expect(otherOwner).toBeVisible();
await list.click();
await currentUserRole.click();

View File

@@ -1,5 +1,3 @@
import crypto from 'crypto';
import { expect, test } from '@playwright/test';
import {
@@ -103,9 +101,8 @@ test.describe('Doc Routing: Not loggued', () => {
page,
browserName,
}) => {
const uuid = crypto.randomUUID();
await mockedDocument(page, { link_reach: 'public', id: uuid });
await page.goto(`/docs/${uuid}/`);
await mockedDocument(page, { link_reach: 'public' });
await page.goto('/docs/mocked-document-id/');
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await page.getByRole('button', { name: 'Login' }).click();
await keyCloakSignIn(page, browserName, false);

View File

@@ -86,7 +86,7 @@ test.describe('Document search', () => {
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Hello world');
await editor.getByText('Hello world').selectText();
await editor.getByText('Hello world').dblclick();
await page.keyboard.press('Control+k');
await expect(page.getByRole('textbox', { name: 'Edit URL' })).toBeVisible();

View File

@@ -101,8 +101,6 @@ test.describe('Doc Visibility: Restricted', () => {
page,
browserName,
}) => {
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -123,16 +121,14 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
await expect(
page.getByText('You do not have permission to view this document.'),
).toBeVisible({
timeout: 10000,
});
).toBeVisible();
});
test('A doc is accessible when member.', async ({ page, browserName }) => {
@@ -177,7 +173,7 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
@@ -434,7 +430,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
@@ -494,7 +490,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);

View File

@@ -1,115 +1,14 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './common';
test.describe('Footer', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks the footer is not displayed if no config', async ({ page }) => {
await overrideConfig(page, {
theme_customization: {},
});
await page.goto('/');
await expect(page.locator('footer')).toBeHidden();
});
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer').first();
await expect(footer.getByAltText('Docs Logo')).toBeVisible();
await expect(footer.getByRole('heading', { name: 'Docs' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'Github' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'DINUM' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'ZenDiS' })).toBeVisible();
await expect(
footer.getByRole('link', { name: 'BlockNote.js' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Legal Notice' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Personal data and cookies' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Accessibility' }),
).toBeVisible();
await expect(
footer.getByText(
'Unless otherwise stated, all content on this site is under licence',
),
).toBeVisible();
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await page.getByLabel('Français').click();
await expect(
page.locator('footer').getByText('Mentions légales'),
).toBeVisible();
});
test('checks the footer is correctly overrided', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
footer: {
default: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: 'Gouvernement Logo',
},
externalLinks: [
{
label: 'legifrance.gouv.fr',
href: '#',
},
{
label: 'info.gouv.fr',
href: '#',
},
],
legalLinks: [
{
label: 'Legal link',
href: '#',
},
],
bottomInformation: {
label: 'Some bottom information text',
link: {
label: 'a custom label',
href: '#',
},
},
},
fr: {
bottomInformation: {
label: "Text d'information en bas de page en français",
link: {
label: 'un label personnalisé',
href: '#',
},
},
},
},
},
});
await page.goto('/');
const footer = page.locator('footer').first();
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
await expect(footer.getByRole('heading', { name: 'Docs' })).toBeHidden();
await expect(footer.getByText('BETA')).toBeHidden();
await expect(
footer.getByRole('link', { name: 'legifrance.gouv.fr' }),
).toBeVisible();
@@ -119,30 +18,53 @@ test.describe('Footer', () => {
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Legal link' }),
footer.getByRole('link', { name: 'service-public.fr' }),
).toBeVisible();
await expect(
footer.getByText('Some bottom information text'),
footer.getByRole('link', { name: 'data.gouv.fr' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'a custom label' }),
).toBeVisible();
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await page.getByLabel('Français').click();
await expect(
page
.locator('footer')
.getByText("Text d'information en bas de page en français"),
footer.getByRole('link', { name: 'Legal Notice' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'un label personnalisé' }),
footer.getByRole('link', { name: 'Personal data and cookies' }),
).toBeVisible();
await expect(
footer.getByRole('link', { name: 'Accessibility' }),
).toBeVisible();
await expect(
footer.getByText(
'Unless otherwise stated, all content on this site is under licence',
),
).toBeVisible();
});
const legalPages = [
{ name: 'Legal Notice', url: '/legal-notice/' },
{ name: 'Personal data and cookies', url: '/personal-data-cookies/' },
{ name: 'Accessibility', url: '/accessibility/' },
];
for (const { name, url } of legalPages) {
test(`checks ${name} page`, async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer').first();
await footer.getByRole('link', { name }).click();
await expect(
page
.getByRole('heading', {
name,
})
.first(),
).toBeVisible();
await expect(page).toHaveURL(url);
});
}
});

View File

@@ -1,39 +1,20 @@
import { expect, test } from '@playwright/test';
import { expectLoginPage, keyCloakSignIn, overrideConfig } from './common';
import { expectLoginPage, keyCloakSignIn } from './common';
test.describe('Header', () => {
test('checks all the elements are visible', async ({ page }) => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('checks all the elements are visible', async ({ page }) => {
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'font-family',
/Roboto/i,
'color',
'rgb(0, 0, 145)',
);
await expect(
header.getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
await expect(header.getByText('English')).toBeVisible();
});
test('checks all the elements are visible with DSFR theme', async ({
page,
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'font-family',
/Marianne/i,
@@ -55,11 +36,6 @@ test.describe('Header', () => {
});
test('checks La Gauffre interaction', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
await page.goto('/');
const header = page.locator('header').first();
await expect(
@@ -92,13 +68,11 @@ test.describe('Header', () => {
test.describe('Header mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
});
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('it checks the header when mobile', async ({ page }) => {
const header = page.locator('header').first();
await expect(header.getByLabel('Open the header menu')).toBeVisible();

View File

@@ -1,7 +1,5 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/docs/');
});
@@ -9,62 +7,6 @@ test.beforeEach(async ({ page }) => {
test.describe('Home page', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/docs/');
// Check header content
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
await expect(
header.getByRole('button', { name: /Language/ }),
).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles
const h2 = page.locator('h2');
await expect(h2.getByText('Govs ❤️ Open Source.')).toBeVisible();
await expect(
h2.getByText('Collaborative writing, Simplified.'),
).toBeVisible();
await expect(
h2.getByText('An uncompromising writing experience.'),
).toBeVisible();
await expect(
h2.getByText('Simple and secure collaboration.'),
).toBeVisible();
await expect(h2.getByText('Flexible export.')).toBeVisible();
await expect(
h2.getByText('A new way to organize knowledge.'),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Start Writing' }),
).toBeVisible();
await expect(footer).toBeVisible();
});
test('checks all the elements are visible with dsfr theme', async ({
page,
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
footer: {
default: {
externalLinks: [
{
label: 'legifrance.gouv.fr',
href: '#',
},
],
},
},
},
});
await page.goto('/docs/');
// Check header content
const header = page.locator('header').first();
const footer = page.locator('footer').first();
@@ -108,17 +50,4 @@ test.describe('Home page', () => {
await expect(footer).toBeVisible();
});
test('it checks the homepage feature flag', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_HOMEPAGE_FEATURE_ENABLED: false,
});
await page.goto('/');
// Keyclock login page
await expect(
page.locator('.login-pf-page-header').getByText('impress'),
).toBeVisible();
});
});

View File

@@ -54,11 +54,18 @@ test.describe.serial('Language', () => {
}) => {
// Helper function to intercept and assert 404 response
const check404Response = async (expectedDetail: string) => {
const interceptedBackendResponse = await page.request.get(
'http://localhost:8071/api/v1.0/documents/non-existent-doc-uuid/',
const expectedBackendResponse = page.waitForResponse(
(response) =>
response.url().includes('/api') &&
response.url().includes('non-existent-doc-uuid') &&
response.status() === 404,
);
// Trigger the specific 404 XHR response by navigating to a non-existent document
await page.goto('/docs/non-existent-doc-uuid');
// Assert that the intercepted error message is in the expected language
const interceptedBackendResponse = await expectedBackendResponse;
expect(await interceptedBackendResponse.json()).toStrictEqual({
detail: expectedDetail,
});

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.3.0",
"version": "3.0.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,9 +12,9 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.52.0",
"@playwright/test": "1.50.1",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"@types/pdf-parse": "1.1.4",
"eslint-config-impress": "*",
"typescript": "*"
},

View File

@@ -1,3 +1,2 @@
NEXT_PUBLIC_API_ORIGIN=
NEXT_PUBLIC_SW_DEACTIVATED=
NEXT_PUBLIC_PUBLISH_AS_MIT=true

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