Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b533b93169 | |||
| c3f81c2b62 | |||
| c7261cf507 | |||
| e504f43611 | |||
| 3ad6d0ea12 | |||
| 9e8a7b3502 | |||
| 05db9c8e51 | |||
| 7ed33019c2 | |||
| a99c813421 | |||
| a83902a0d4 | |||
| 080f855083 | |||
| 90d94f6b7a | |||
| f97ab51c8e | |||
| ba4f90a607 | |||
| 6c16e081de | |||
| 56a945983e | |||
| 4fbbead405 | |||
| 9a212400a0 | |||
| f07fcd4c0d | |||
| 4fc49d5cb2 | |||
| 0fd16b4371 | |||
| fbb2799050 | |||
| afbb4b29dc | |||
| db63ebd0c8 | |||
| c5f018e03e | |||
| 1c93fbc007 | |||
| d811e3c2fc | |||
| fe5fda5d73 | |||
| bf66265125 | |||
| ce329142dc | |||
| f8cff43dac | |||
| f5b2c27bd8 | |||
| 62433ef7f1 | |||
| bc0824d110 | |||
| fa653c6776 | |||
| d12f942d29 | |||
| 62f85e7d24 | |||
| 65cc088a17 | |||
| 94e99784f3 | |||
| fa83955a77 | |||
| 5962f7aae1 | |||
| dc06315566 | |||
| f4ad26a8fa | |||
| d952815932 | |||
| cde64ed80a | |||
| cfd88d0469 | |||
| 5e45fec296 | |||
| 393e7a06e2 | |||
| f1af87baf8 | |||
| f851ef2d85 | |||
| 252ab6a586 | |||
| cf2a02c8de | |||
| d87a2ed4eb | |||
| c9d053d1c0 | |||
| b5f0f06ea3 | |||
| 36b0ff9f63 | |||
| 7a383957a7 | |||
| b5630359ee | |||
| 310154815b | |||
| 2733785016 | |||
| 99ba414d88 | |||
| 41631b5b70 | |||
| 6ca654bf1a | |||
| 074585337b | |||
| f1b398e1ae | |||
| d1f73f18cd | |||
| 3f2d84bf62 | |||
| 7b9c362d38 | |||
| bf999979d2 | |||
| 09d3ff3754 | |||
| 6e5d005dee | |||
| 6377c8fcca | |||
| 3c8cacc048 | |||
| 598fb4fa27 | |||
| 51618ad081 | |||
| 8109d5ba08 | |||
| e4d0179bbe | |||
| 9d3dfb6de7 | |||
| 0da042f887 | |||
| 6cd0cd0689 | |||
| 10b088599c | |||
| 62d1bc6473 | |||
| fc1d33268c | |||
| 95833fa5ec | |||
| dd6e0b5072 | |||
| 95d3a8cd18 | |||
| 4f126ab824 | |||
| fb90c13dad | |||
| 4118d79525 | |||
| 5848f43cb4 | |||
| 4b0fd223c8 | |||
| 31d0733851 | |||
| 16e20e984c | |||
| 76c28760dc | |||
| d856abb5d8 | |||
| 25abd964de | |||
| a070e1dd87 | |||
| 37d9ae8cca | |||
| 29ea6b8ef7 | |||
| a692fa6f39 | |||
| 4d541c5d52 | |||
| e5f029ad1d | |||
| bd79f84e07 | |||
| a070f56339 | |||
| 02478acb3f | |||
| 23aa497db0 | |||
| d48436bffb | |||
| 41e4c45934 | |||
| 6be87ed477 | |||
| c96182b3e3 | |||
| e79d1d618a | |||
| 2691cdd4a2 | |||
| 05a1390bdc | |||
| dfe8ae14fe | |||
| 74165f6890 | |||
| 349cbf8eb3 | |||
| 12ef1a2450 |
@@ -34,3 +34,4 @@ db.sqlite3
|
||||
|
||||
# Frontend
|
||||
node_modules
|
||||
.next
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Set the default behavior for all files
|
||||
* text=auto eol=lf
|
||||
|
||||
# Binary files (should not be modified)
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.mov binary
|
||||
*.mp4 binary
|
||||
*.mp3 binary
|
||||
*.flv binary
|
||||
*.fla binary
|
||||
*.swf binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.ttf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.eot binary
|
||||
*.pdf binary
|
||||
@@ -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**
|
||||
- Impress version:
|
||||
- Platform:
|
||||
- Docs version:
|
||||
- Instance url:
|
||||
|
||||
**Possible Solution**
|
||||
<!--- Only if you have suggestions on a fix for the bug -->
|
||||
|
||||
@@ -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 a version the docs (if applicable).
|
||||
Maybe a screenshot or design?
|
||||
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?
|
||||
|
||||
**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! -->
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
---
|
||||
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).
|
||||
|
||||
<!-- ^ 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).
|
||||
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).
|
||||
|
||||
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌
|
||||
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
## Purpose
|
||||
|
||||
Description...
|
||||
Describe the purpose of this pull request.
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
Description...
|
||||
- [ ] item 1...
|
||||
- [ ] item 2...
|
||||
|
||||
- [] item 1...
|
||||
- [] item 2...
|
||||
## External contributions
|
||||
|
||||
Thank you for your contribution! 🎉
|
||||
|
||||
Please ensure the following items are checked before submitting your pull request:
|
||||
- [ ] I have read and followed the [contributing guidelines](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I have read and agreed to the [Code of Conduct](https://github.com/suitenumerique/docs/blob/main/CODE_OF_CONDUCT.md)
|
||||
- [ ] I have signed off my commits with `git commit --signoff` (DCO compliance)
|
||||
- [ ] I have signed my commits with my SSH or GPG key (`git commit -S`)
|
||||
- [ ] My commit messages follow the required format: `<gitmoji>(type) title description`
|
||||
- [ ] I have added a changelog entry under `## [Unreleased]` section (if noticeable change)
|
||||
- [ ] I have added corresponding tests for new features or bug fixes (if applicable)
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '20.x'
|
||||
node_version: '22.x'
|
||||
with-front-dependencies-installation: true
|
||||
|
||||
synchronize-with-crowdin:
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '20.x'
|
||||
node_version: '22.x'
|
||||
with-front-dependencies-installation: true
|
||||
with-build_mails: true
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.12.6"
|
||||
python-version: "3.13.3"
|
||||
- name: Upgrade pip and setuptools
|
||||
run: pip install --upgrade pip setuptools
|
||||
- name: Install development dependencies
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: '20.x'
|
||||
default: '22.x'
|
||||
type: string
|
||||
with-front-dependencies-installation:
|
||||
type: boolean
|
||||
|
||||
@@ -79,7 +79,9 @@ jobs:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
build-args: |
|
||||
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
PUBLISH_AS_MIT=false
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '20.x'
|
||||
node_version: '22.x'
|
||||
with-front-dependencies-installation: true
|
||||
|
||||
test-front:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
node-version: "22.x"
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
node-version: "22.x"
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
node-version: "22.x"
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright chromium
|
||||
|
||||
- name: Start Docker services
|
||||
run: make bootstrap FLUSH_ARGS='--no-input' cache=
|
||||
run: make bootstrap-e2e FLUSH_ARGS='--no-input'
|
||||
|
||||
- name: Run e2e tests
|
||||
run: cd src/frontend/ && yarn e2e:test --project='chromium'
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
node-version: "22.x"
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright firefox webkit chromium
|
||||
|
||||
- name: Start Docker services
|
||||
run: make bootstrap FLUSH_ARGS='--no-input' cache=
|
||||
run: make bootstrap-e2e FLUSH_ARGS='--no-input'
|
||||
|
||||
- name: Run e2e tests
|
||||
run: cd src/frontend/ && yarn e2e:test --project=firefox --project=webkit
|
||||
|
||||
@@ -61,6 +61,25 @@ 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:
|
||||
@@ -72,7 +91,7 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.12.6"
|
||||
python-version: "3.13.3"
|
||||
- name: Upgrade pip and setuptools
|
||||
run: pip install --upgrade pip setuptools
|
||||
- name: Install development dependencies
|
||||
@@ -104,6 +123,9 @@ jobs:
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
redis:
|
||||
image: redis:5
|
||||
|
||||
env:
|
||||
DJANGO_CONFIGURATION: Test
|
||||
DJANGO_SETTINGS_MODULE: impress.settings
|
||||
@@ -167,7 +189,7 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.12.6"
|
||||
python-version: "3.13.3"
|
||||
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
|
||||
+73
-9
@@ -8,28 +8,94 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## Fixed
|
||||
### Added
|
||||
|
||||
- 🔒(frontend) enhance file download security #889
|
||||
- ✨(frontend) add customization for translations #857
|
||||
- 📝(project) add troubleshoot doc #1066
|
||||
- 📝(project) add system-requirement doc #1066
|
||||
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
|
||||
|
||||
### Changed
|
||||
|
||||
- 📌(yjs) stop pinning node to minor version on yjs docker image #1005
|
||||
- 🧑💻(docker) add .next to .dockerignore #1055
|
||||
- 🧑💻(docker) handle frontend development images with docker compose #1033
|
||||
- 🧑💻(docker) add y-provider config to development environment #1057
|
||||
|
||||
### Fixed
|
||||
|
||||
-🐛(frontend) table of content disappearing #982
|
||||
-🐛(frontend) fix multiple EmojiPicker #1012
|
||||
-🐛(frontend) fix meta title #1017
|
||||
-🔧(git) set LF line endings for all text files #1032
|
||||
-📝(docs) minor fixes to docs/env.md
|
||||
|
||||
## [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
|
||||
|
||||
@@ -158,11 +224,6 @@ and this project adheres to
|
||||
- ♻️(frontend) improve table pdf rendering
|
||||
- 🐛(email) invitation emails in receivers language
|
||||
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(backend) race condition create doc #633
|
||||
|
||||
## [2.2.0] - 2025-02-10
|
||||
|
||||
## Added
|
||||
@@ -533,7 +594,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 occurences of pad to doc (#99)
|
||||
- ✏️(frontend) change all occurrences of pad to doc (#99)
|
||||
|
||||
## Fixed
|
||||
|
||||
@@ -551,7 +612,10 @@ 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.1.0...main
|
||||
[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
|
||||
[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
|
||||
|
||||
+11
-7
@@ -42,34 +42,38 @@ 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 this
|
||||
- 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
|
||||
|
||||
## 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.
|
||||
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).
|
||||
|
||||
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.
|
||||
+21
-7
@@ -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.
|
||||
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.
|
||||
|
||||
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/).
|
||||
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`.
|
||||
|
||||
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 😊.
|
||||
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.
|
||||
|
||||
## Creating an Issue
|
||||
|
||||
@@ -35,10 +35,14 @@ 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.
|
||||
* **description**: Include additional details about what was changed and why.
|
||||
* **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**
|
||||
|
||||
### Example Commit Message
|
||||
|
||||
@@ -66,7 +70,9 @@ 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:
|
||||
- check your commits
|
||||
- signoff your commits
|
||||
- sign your commits with your key (SSH, GPG etc.)
|
||||
- check your commits (see warnings above)
|
||||
- check the linting: `make lint && make frontend-lint`
|
||||
- check the tests: `make test`
|
||||
- add a changelog entry
|
||||
@@ -86,3 +92,11 @@ 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).
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
# Django impress
|
||||
|
||||
# ---- base image to inherit from ----
|
||||
FROM python:3.12.6-alpine3.20 AS base
|
||||
FROM python:3.13.3-alpine 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:20 AS mail-builder
|
||||
FROM node:24 AS mail-builder
|
||||
|
||||
COPY ./src/mail /mail/app
|
||||
|
||||
@@ -139,6 +139,9 @@ 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
|
||||
|
||||
@@ -39,6 +39,7 @@ DOCKER_UID = $(shell id -u)
|
||||
DOCKER_GID = $(shell id -g)
|
||||
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
|
||||
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
|
||||
COMPOSE_E2E = DOCKER_USER=$(DOCKER_USER) docker compose -f compose.yml -f compose-e2e.yml
|
||||
COMPOSE_EXEC = $(COMPOSE) exec
|
||||
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev
|
||||
COMPOSE_RUN = $(COMPOSE) run --rm
|
||||
@@ -74,22 +75,39 @@ create-env-files: \
|
||||
env.d/development/kc_postgresql
|
||||
.PHONY: create-env-files
|
||||
|
||||
bootstrap: ## Prepare Docker images for the project
|
||||
bootstrap: \
|
||||
pre-bootstrap: \
|
||||
data/media \
|
||||
data/static \
|
||||
create-env-files \
|
||||
build \
|
||||
create-env-files
|
||||
.PHONY: pre-bootstrap
|
||||
|
||||
post-bootstrap: \
|
||||
migrate \
|
||||
demo \
|
||||
back-i18n-compile \
|
||||
mails-install \
|
||||
mails-build \
|
||||
mails-build
|
||||
.PHONY: post-bootstrap
|
||||
|
||||
|
||||
bootstrap: ## Prepare Docker developmentimages for the project
|
||||
bootstrap: \
|
||||
pre-bootstrap \
|
||||
build \
|
||||
post-bootstrap \
|
||||
run
|
||||
.PHONY: bootstrap
|
||||
|
||||
bootstrap-e2e: ## Prepare Docker production images to be used for e2e tests
|
||||
bootstrap-e2e: \
|
||||
pre-bootstrap \
|
||||
build-e2e \
|
||||
post-bootstrap \
|
||||
run-e2e
|
||||
.PHONY: bootstrap-e2e
|
||||
|
||||
# -- Docker/compose
|
||||
build: cache ?= --no-cache
|
||||
build: cache ?=
|
||||
build: ## build the project containers
|
||||
@$(MAKE) build-backend cache=$(cache)
|
||||
@$(MAKE) build-yjs-provider cache=$(cache)
|
||||
@@ -103,16 +121,23 @@ build-backend: ## build the app-dev container
|
||||
|
||||
build-yjs-provider: cache ?=
|
||||
build-yjs-provider: ## build the y-provider container
|
||||
@$(COMPOSE) build y-provider $(cache)
|
||||
@$(COMPOSE) build y-provider-development $(cache)
|
||||
.PHONY: build-yjs-provider
|
||||
|
||||
build-frontend: cache ?=
|
||||
build-frontend: ## build the frontend container
|
||||
@$(COMPOSE) build frontend $(cache)
|
||||
@$(COMPOSE) build frontend-development $(cache)
|
||||
.PHONY: build-frontend
|
||||
|
||||
build-e2e: cache ?=
|
||||
build-e2e: ## build the e2e container
|
||||
@$(MAKE) build-backend cache=$(cache)
|
||||
@$(COMPOSE_E2E) build frontend $(cache)
|
||||
@$(COMPOSE_E2E) build y-provider $(cache)
|
||||
.PHONY: build-e2e
|
||||
|
||||
down: ## stop and remove containers, networks, images, and volumes
|
||||
@$(COMPOSE) down
|
||||
@$(COMPOSE_E2E) down
|
||||
.PHONY: down
|
||||
|
||||
logs: ## display app-dev logs (follow mode)
|
||||
@@ -121,22 +146,30 @@ logs: ## display app-dev logs (follow mode)
|
||||
|
||||
run-backend: ## Start only the backend application and all needed services
|
||||
@$(COMPOSE) up --force-recreate -d celery-dev
|
||||
@$(COMPOSE) up --force-recreate -d y-provider
|
||||
@$(COMPOSE) up --force-recreate -d y-provider-development
|
||||
@$(COMPOSE) up --force-recreate -d nginx
|
||||
.PHONY: run-backend
|
||||
|
||||
run: ## start the wsgi (production) and development server
|
||||
run:
|
||||
@$(MAKE) run-backend
|
||||
@$(COMPOSE) up --force-recreate -d frontend
|
||||
@$(COMPOSE) up --force-recreate -d frontend-development
|
||||
.PHONY: run
|
||||
|
||||
run-e2e: ## start the e2e server
|
||||
run-e2e:
|
||||
@$(MAKE) run-backend
|
||||
@$(COMPOSE_E2E) stop y-provider-development
|
||||
@$(COMPOSE_E2E) up --force-recreate -d frontend
|
||||
@$(COMPOSE_E2E) up --force-recreate -d y-provider
|
||||
.PHONY: run-e2e
|
||||
|
||||
status: ## an alias for "docker compose ps"
|
||||
@$(COMPOSE) ps
|
||||
@$(COMPOSE_E2E) ps
|
||||
.PHONY: status
|
||||
|
||||
stop: ## stop the development server using Docker
|
||||
@$(COMPOSE) stop
|
||||
@$(COMPOSE_E2E) stop
|
||||
.PHONY: stop
|
||||
|
||||
# -- Backend
|
||||
@@ -315,7 +348,7 @@ frontend-lint: ## run the frontend linter
|
||||
.PHONY: frontend-lint
|
||||
|
||||
run-frontend-development: ## Run the frontend in development mode
|
||||
@$(COMPOSE) stop frontend
|
||||
@$(COMPOSE) stop frontend frontend-development
|
||||
cd $(PATH_FRONT_IMPRESS) && yarn dev
|
||||
.PHONY: run-frontend-development
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/suitenumerique/docs">
|
||||
<img alt="Docs" src="/docs/assets/docs-logo.png" width="300" />
|
||||
<img alt="Docs" src="/docs/assets/banner-docs.png" width="100%" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Welcome to Docs! The open source document editor where your notes can become knowledge through live collaboration
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://matrix.to/#/#docs-official:matrix.org">
|
||||
Chat on Matrix
|
||||
@@ -20,43 +26,52 @@ Welcome to Docs! The open source document editor where your notes can become kno
|
||||
</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 ❓
|
||||
|
||||
Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing.
|
||||
|
||||
### Write
|
||||
* 😌 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)
|
||||
It offers a scalable and secure alternative to tools such as Google Docs, Notion (without the dbs), Outline, or Confluence.
|
||||
|
||||
### 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 05/2025`
|
||||
### 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!
|
||||
|
||||
### 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.
|
||||
|
||||
### Self-host
|
||||
* 🚀 Easy to install, scalable and secure alternative to Notion, Outline or Confluence
|
||||
🚀 Docs is easy to install on your own servers
|
||||
|
||||
⚠️ For the PDF and Docx export Docs relies on XL packages from BlockNote licenced in AGPL-3.0. Please make sure you fulfill your obligations regarding BlockNote licensing (see https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE and https://www.blocknotejs.org/about#partner-with-us).
|
||||
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.
|
||||
|
||||
## Getting started 🔧
|
||||
|
||||
### Test it
|
||||
|
||||
Test Docs on your browser by visiting this [demo document](https://impress-preprod.beta.numerique.gouv.fr/docs/6ee5aac4-4fb9-457d-95bf-bb56c2467713/)
|
||||
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/)
|
||||
|
||||
### Run it locally
|
||||
### Run Docs 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.
|
||||
> ⚠️ 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.
|
||||
|
||||
**Prerequisite**
|
||||
|
||||
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop:
|
||||
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop, then type:
|
||||
|
||||
```shellscript
|
||||
$ docker -v
|
||||
@@ -68,7 +83,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 `docker` group.
|
||||
> ⚠️ You may need to run the following commands with `sudo`, but this can be avoided by adding your user to the local `docker` group.
|
||||
|
||||
**Project bootstrap**
|
||||
|
||||
@@ -78,20 +93,20 @@ 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 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.
|
||||
This command builds the `app-dev` and `frontend-dev` containers, 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.
|
||||
|
||||
Your Docker services should now be up and running 🎉
|
||||
|
||||
You can access to the project by going to <http://localhost:3000>.
|
||||
You can access 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
|
||||
password: impress
|
||||
```
|
||||
|
||||
📝 Note that if you need to run them afterwards, you can use the eponym Make rule:
|
||||
📝 Note that if you need to run them afterwards, you can use the eponymous Make rule:
|
||||
|
||||
```shellscript
|
||||
$ make run
|
||||
@@ -119,13 +134,13 @@ $ make run-backend
|
||||
|
||||
**Adding content**
|
||||
|
||||
You can create a basic demo site by running:
|
||||
You can create a basic demo site by running this command:
|
||||
|
||||
```shellscript
|
||||
$ make demo
|
||||
```
|
||||
|
||||
Finally, you can check all available Make rules using:
|
||||
Finally, you can check all available Make rules using this command:
|
||||
|
||||
```shellscript
|
||||
$ make help
|
||||
@@ -133,7 +148,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>.
|
||||
|
||||
@@ -145,7 +160,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
|
||||
|
||||
@@ -155,7 +170,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 🙌
|
||||
|
||||
@@ -163,9 +178,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
|
||||
@@ -183,14 +198,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/).
|
||||
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/).
|
||||
|
||||
|
||||
### 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">
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
Security is very important to us.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
We appreciate your effort to make Docs more secure.
|
||||
|
||||
|
||||
@@ -16,6 +16,15 @@ 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/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`)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ REPO_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
|
||||
UNSET_USER=0
|
||||
|
||||
TERRAFORM_DIRECTORY="./env.d/terraform"
|
||||
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
|
||||
COMPOSE_FILE="${REPO_DIR}/compose.yml"
|
||||
|
||||
|
||||
# _set_user: set (or unset) default user id used to run docker commands
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
services:
|
||||
|
||||
frontend:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
args:
|
||||
API_ORIGIN: "http://localhost:8071"
|
||||
PUBLISH_AS_MIT: "false"
|
||||
SW_DEACTIVATED: "true"
|
||||
image: impress:frontend-production
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
y-provider:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
image: impress:y-provider-production
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
ports:
|
||||
- "4444:4444"
|
||||
@@ -98,40 +98,6 @@ services:
|
||||
depends_on:
|
||||
- app-dev
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: backend-production
|
||||
args:
|
||||
DOCKER_USER: ${DOCKER_USER:-1000}
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: impress:backend-production
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=Demo
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/postgresql
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
redis:
|
||||
condition: service_started
|
||||
minio:
|
||||
condition: service_started
|
||||
|
||||
celery:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: impress:backend-production
|
||||
command: ["celery", "-A", "impress.celery_app", "worker", "-l", "INFO"]
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=Demo
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/postgresql
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
nginx:
|
||||
image: nginx:1.25
|
||||
ports:
|
||||
@@ -141,24 +107,25 @@ services:
|
||||
depends_on:
|
||||
app-dev:
|
||||
condition: service_started
|
||||
y-provider:
|
||||
condition: service_started
|
||||
keycloak:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
|
||||
frontend:
|
||||
frontend-development:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
target: impress-dev
|
||||
args:
|
||||
API_ORIGIN: "http://localhost:8071"
|
||||
Y_PROVIDER_URL: "ws://localhost:4444"
|
||||
MEDIA_URL: "http://localhost:8083"
|
||||
PUBLISH_AS_MIT: "false"
|
||||
SW_DEACTIVATED: "true"
|
||||
image: impress:frontend-development
|
||||
volumes:
|
||||
- ./src/frontend:/home/frontend
|
||||
- /home/frontend/node_modules
|
||||
- /home/frontend/apps/impress/node_modules
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
@@ -172,28 +139,29 @@ services:
|
||||
working_dir: /app
|
||||
|
||||
node:
|
||||
image: node:18
|
||||
image: node:22
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
volumes:
|
||||
- ".:/app"
|
||||
|
||||
y-provider:
|
||||
y-provider-development:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
command: ["yarn", "workspace", "server-y-provider", "run", "dev"]
|
||||
working_dir: /app/frontend
|
||||
target: y-provider-development
|
||||
image: impress:y-provider-development
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
ports:
|
||||
- "4444:4444"
|
||||
volumes:
|
||||
- ./src/frontend/:/app/frontend
|
||||
- ./src/frontend/:/home/frontend
|
||||
- /home/frontend/node_modules
|
||||
- /home/frontend/servers/y-provider/node_modules
|
||||
|
||||
kc_postgresql:
|
||||
image: postgres:14.3
|
||||
@@ -60,7 +60,7 @@
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-chromium",
|
||||
"email": "user@chromium.e2e",
|
||||
"email": "user@chromium.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Chromium",
|
||||
"enabled": true,
|
||||
@@ -74,7 +74,7 @@
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-webkit",
|
||||
"email": "user@webkit.e2e",
|
||||
"email": "user@webkit.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Webkit",
|
||||
"enabled": true,
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-firefox",
|
||||
"email": "user@firefox.e2e",
|
||||
"email": "user@firefox.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Firefox",
|
||||
"enabled": true,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+135
-92
@@ -4,97 +4,140 @@ 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 environment variables you can set for the `impress-backend` container.
|
||||
|
||||
| Option | Description | default |
|
||||
|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
||||
| AI_API_KEY | AI key to be used for AI Base url | |
|
||||
| AI_BASE_URL | OpenAI compatible AI base url | |
|
||||
| AI_FEATURE_ENABLED | Enable AI options | false |
|
||||
| AI_MODEL | AI Model to use | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
|
||||
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
|
||||
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
|
||||
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
|
||||
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
|
||||
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
|
||||
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
|
||||
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
|
||||
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
|
||||
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
|
||||
| COLLABORATION_API_URL | Collaboration api host | |
|
||||
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
|
||||
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
|
||||
| COLLABORATION_WS_URL | Collaboration websocket url | |
|
||||
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
|
||||
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown |
|
||||
| CONVERSION_API_SECURE | Require secure conversion api | false |
|
||||
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
|
||||
| CRISP_WEBSITE_ID | Crisp website id for support | |
|
||||
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_HOST | Host of the database | localhost |
|
||||
| DB_NAME | Name of the database | impress |
|
||||
| DB_PASSWORD | Password to authenticate with | pass |
|
||||
| DB_PORT | Port of the database | 5432 |
|
||||
| DB_USER | User to authenticate with | dinum |
|
||||
| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker url | redis://redis:6379/0 |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | List of origins allowed for CORS using regulair expressions | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | List of origins allowed for CORS | [] |
|
||||
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_BRAND_NAME | Brand name for email | |
|
||||
| DJANGO_EMAIL_FROM | Email address used as sender | from@example.com |
|
||||
| DJANGO_EMAIL_HOST | Hostname of email | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
|
||||
| DJANGO_EMAIL_PORT | Port used to connect to email host | |
|
||||
| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false |
|
||||
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
|
||||
| DJANGO_SECRET_KEY | Secret key | |
|
||||
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
|
||||
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
|
||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_THEME | Frontend theme to use | |
|
||||
| LANGUAGE_CODE | Default language | en-us |
|
||||
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGIN_REDIRECT_URL | Login redirect url | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect url on failure | |
|
||||
| LOGOUT_REDIRECT_URL | Logout redirect url | |
|
||||
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
|
||||
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
||||
| MEDIA_BASE_URL | | |
|
||||
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
||||
| OIDC_CREATE_USER | Create used on OIDC | false |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | true |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
|
||||
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
|
||||
| OIDC_RP_CLIENT_ID | Client id used for OIDC | impress |
|
||||
| OIDC_RP_CLIENT_SECRET | Client secret used for OIDC | |
|
||||
| OIDC_RP_SCOPES | Scopes requested for OIDC | openid email |
|
||||
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
|
||||
| POSTHOG_KEY | Posthog key for analytics | |
|
||||
| REDIS_URL | Cache url | redis://redis:6379/1 |
|
||||
| SENTRY_DSN | Sentry host | |
|
||||
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
|
||||
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
|
||||
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
|
||||
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
|
||||
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
|
||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||
|
||||
|
||||
## impress-frontend image
|
||||
|
||||
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 |
|
||||
|-------------------------------------------------|-----------------------------------------------------------------------------------------------|---------------------------------------------------------|
|
||||
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
||||
| AI_API_KEY | Ai key to be used for AI Base url | |
|
||||
| AI_BASE_URL | Openai compatible AI base url | |
|
||||
| AI_MODEL | Ai Model to use | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
|
||||
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
|
||||
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
|
||||
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
|
||||
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
|
||||
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
|
||||
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
|
||||
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
|
||||
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
|
||||
| COLLABORATION_API_URL | Collaboration api host | |
|
||||
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
|
||||
| COLLABORATION_WS_URL | Collaboration websocket url | |
|
||||
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
|
||||
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown |
|
||||
| CONVERSION_API_SECURE | Require secure conversion api | false |
|
||||
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
|
||||
| CRISP_WEBSITE_ID | Crisp website id for support | |
|
||||
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_HOST | Host of the database | localhost |
|
||||
| DB_NAME | Name of the database | impress |
|
||||
| DB_PASSWORD | Password to authenticate with | pass |
|
||||
| DB_PORT | Port of the database | 5432 |
|
||||
| DB_USER | User to authenticate with | dinum |
|
||||
| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker url | redis://redis:6379/0 |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | true |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | List of origins allowed for CORS using regulair expressions | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | List of origins allowed for CORS | [] |
|
||||
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_BRAND_NAME | Brand name for email | |
|
||||
| DJANGO_EMAIL_FROM | Email adress used as sender | from@example.com |
|
||||
| DJANGO_EMAIL_HOST | Host name of email | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
|
||||
| DJANGO_EMAIL_PORT | Port used to connect to email host | |
|
||||
| DJANGO_EMAIL_USE_SSL | Use sstl for email host connection | false |
|
||||
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
|
||||
| DJANGO_SECRET_KEY | Secret key | |
|
||||
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
|
||||
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
|
||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||
| FRONTEND_FOOTER_FEATURE_ENABLED | Frontend feature flag to display the footer | false |
|
||||
| FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT | Cache duration of the json footer | 86400 |
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_THEME | Frontend theme to use | |
|
||||
| FRONTEND_URL_JSON_FOOTER | Url with a json to configure the footer | |
|
||||
| LANGUAGE_CODE | Default language | en-us |
|
||||
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGIN_REDIRECT_URL | Login redirect url | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect url on failure | |
|
||||
| LOGOUT_REDIRECT_URL | Logout redirect url | |
|
||||
| MEDIA_BASE_URL | | |
|
||||
| Oidc_ALLOW_DUPLICATE_EMAILS | Allow dupplicate emails | false |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth paramaters | {} |
|
||||
| OIDC_CREATE_USER | Create used on OIDC | false |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | true |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | Autorization endpoint for OIDC | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
|
||||
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
|
||||
| OIDC_RP_CLIENT_ID | Client id used for OIDC | impress |
|
||||
| OIDC_RP_CLIENT_SECRET | Client secret used for OIDC | |
|
||||
| OIDC_RP_SCOPES | Scopes requested for OIDC | openid email |
|
||||
| OIDC_RP_SIGN_ALGO | Verification algorithm used OIDC tokens | RS256 |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
|
||||
| POSTHOG_KEY | Posthog key for analytics | |
|
||||
| REDIS_URL | Cache url | redis://redis:6379/1 |
|
||||
| SENTRY_DSN | Sentry host | |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
|
||||
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
|
||||
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
|
||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| 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.
|
||||
|
||||
|
||||
@@ -82,13 +82,13 @@ backend:
|
||||
python manage.py createsuperuser --email admin@example.com --password admin
|
||||
restartPolicy: Never
|
||||
|
||||
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumeMounts:
|
||||
- name: certs
|
||||
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
|
||||
subPath: cacert.pem
|
||||
|
||||
# Exra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||
extraVolumes:
|
||||
- name: certs
|
||||
configMap:
|
||||
|
||||
@@ -91,7 +91,7 @@ extraDeploy:
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-chromium",
|
||||
"email": "user@chromium.e2e",
|
||||
"email": "user@chromium.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Chromium",
|
||||
"enabled": "true",
|
||||
@@ -105,7 +105,7 @@ extraDeploy:
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-webkit",
|
||||
"email": "user@webkit.e2e",
|
||||
"email": "user@webkit.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Webkit",
|
||||
"enabled": "true",
|
||||
@@ -119,7 +119,7 @@ extraDeploy:
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-firefox",
|
||||
"email": "user@firefox.e2e",
|
||||
"email": "user@firefox.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Firefox",
|
||||
"enabled": "true",
|
||||
|
||||
@@ -133,7 +133,7 @@ OIDC_RP_SCOPES: "openid email"
|
||||
|
||||
You can find these values in **examples/keycloak.values.yaml**
|
||||
|
||||
### Find redis server connexion values
|
||||
### Find redis server connection values
|
||||
|
||||
Docs needs a redis so we start by deploying one:
|
||||
|
||||
@@ -146,7 +146,7 @@ keycloak-postgresql-0 1/1 Running 0 26m
|
||||
redis-master-0 1/1 Running 0 35s
|
||||
```
|
||||
|
||||
### Find postgresql connexion values
|
||||
### Find postgresql connection 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:
|
||||
|
||||
@@ -173,7 +173,7 @@ POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
```
|
||||
|
||||
### Find s3 bucket connexion values
|
||||
### Find s3 bucket connection 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:
|
||||
|
||||
@@ -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 informations to the helm chart.
|
||||
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.
|
||||
|
||||
```
|
||||
$ helm repo add impress https://suitenumerique.github.io/docs/
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# La Suite Docs – System & Requirements (2025-06)
|
||||
|
||||
## 1. Quick-Reference Matrix (single VM / laptop)
|
||||
|
||||
| Scenario | RAM | vCPU | SSD | Notes |
|
||||
| ------------------------- | ----- | ---- | ------- | ------------------------- |
|
||||
| **Solo dev** | 8 GB | 4 | 15 GB | Hot-reload + one IDE |
|
||||
| **Team QA** | 16 GB | 6 | 30 GB | Runs integration tests |
|
||||
| **Prod ≤ 100 live users** | 32 GB | 8 + | 50 GB + | Scale linearly above this |
|
||||
|
||||
Memory is the first bottleneck; CPU matters only when Celery or the Next.js build is saturated.
|
||||
|
||||
> **Note:** Memory consumption varies by operating system. Windows tends to be more memory-hungry than Linux, so consider adding 10-20% extra RAM when running on Windows compared to Linux-based systems.
|
||||
|
||||
## 2. Development Environment Memory Requirements
|
||||
|
||||
| Service | Typical use | Rationale / source |
|
||||
| ------------------------ | ----------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| PostgreSQL | **1 – 2 GB** | `shared_buffers` starting point ≈ 25% RAM ([postgresql.org][1]) |
|
||||
| Keycloak | **≈ 1.3 GB** | 70% of limit for heap + ~300 MB non-heap ([keycloak.org][2]) |
|
||||
| Redis | **≤ 256 MB** | Empty instance ≈ 3 MB; budget 256 MB to allow small datasets ([stackoverflow.com][3]) |
|
||||
| MinIO | **2 GB (dev) / 32 GB (prod)**| Pre-allocates 1–2 GiB; docs recommend 32 GB per host for ≤ 100 Ti storage ([min.io][4]) |
|
||||
| Django API (+ Celery) | **0.8 – 1.5 GB** | Empirical in-house metrics |
|
||||
| Next.js frontend | **0.5 – 1 GB** | Dev build chain |
|
||||
| Y-Provider (y-websocket) | **< 200 MB** | Large 40 MB YDoc called “big” in community thread ([discuss.yjs.dev][5]) |
|
||||
| Nginx | **< 100 MB** | Static reverse-proxy footprint |
|
||||
|
||||
[1]: https://www.postgresql.org/docs/9.1/runtime-config-resource.html "PostgreSQL: Documentation: 9.1: Resource Consumption"
|
||||
[2]: https://www.keycloak.org/high-availability/concepts-memory-and-cpu-sizing "Concepts for sizing CPU and memory resources - Keycloak"
|
||||
[3]: https://stackoverflow.com/questions/45233052/memory-footprint-for-redis-empty-instance "Memory footprint for Redis empty instance - Stack Overflow"
|
||||
[4]: https://min.io/docs/minio/kubernetes/upstream/operations/checklists/hardware.html "Hardware Checklist — MinIO Object Storage for Kubernetes"
|
||||
[5]: https://discuss.yjs.dev/t/understanding-memory-requirements-for-production-usage/198 "Understanding memory requirements for production usage - Yjs Community"
|
||||
|
||||
> **Rule of thumb:** add 2 GB for OS/overhead, then sum only the rows you actually run.
|
||||
|
||||
## 3. Production Environment Memory Requirements
|
||||
|
||||
Production deployments differ significantly from development environments. The table below shows typical memory usage for production services:
|
||||
|
||||
| Service | Typical use | Rationale / notes |
|
||||
| ------------------------ | ----------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| PostgreSQL | **2 – 8 GB** | Higher `shared_buffers` and connection pooling for concurrent users |
|
||||
| OIDC Provider (optional) | **Variable** | Any OIDC-compatible provider (Keycloak, Auth0, Azure AD, etc.) - external or self-hosted |
|
||||
| Redis | **256 MB – 2 GB** | Session storage and caching; scales with active user sessions |
|
||||
| Object Storage (optional)| **External or self-hosted** | Can use AWS S3, Azure Blob, Google Cloud Storage, or self-hosted MinIO |
|
||||
| Django API (+ Celery) | **1 – 3 GB** | Production workloads with background tasks and higher concurrency |
|
||||
| Static Files (Nginx) | **< 200 MB** | Serves Next.js build output and static assets; no development overhead |
|
||||
| Y-Provider (y-websocket) | **200 MB – 1 GB** | Scales with concurrent document editing sessions |
|
||||
| Nginx (Load Balancer) | **< 200 MB** | Reverse proxy, SSL termination, static file serving |
|
||||
|
||||
### Production Architecture Notes
|
||||
|
||||
- **Frontend**: Uses pre-built Next.js static assets served by Nginx (no Node.js runtime needed)
|
||||
- **Authentication**: Any OIDC-compatible provider can be used instead of self-hosted Keycloak
|
||||
- **Object Storage**: External services (S3, Azure Blob) or self-hosted solutions (MinIO) are both viable
|
||||
- **Database**: Consider PostgreSQL clustering or managed database services for high availability
|
||||
- **Scaling**: Horizontal scaling is recommended for Django API and Y-Provider services
|
||||
|
||||
### Minimal Production Setup (Core Services Only)
|
||||
|
||||
| Service | Memory | Notes |
|
||||
| ------------------------ | --------- | --------------------------------------- |
|
||||
| PostgreSQL | **2 GB** | Core database |
|
||||
| Django API (+ Celery) | **1.5 GB**| Backend services |
|
||||
| Y-Provider | **200 MB**| Real-time collaboration |
|
||||
| Nginx | **100 MB**| Static files + reverse proxy |
|
||||
| Redis | **256 MB**| Session storage |
|
||||
| **Total (without auth/storage)** | **≈ 4 GB** | External OIDC + object storage assumed |
|
||||
|
||||
## 4. Recommended Software Versions
|
||||
|
||||
| Tool | Minimum |
|
||||
| ----------------------- | ------- |
|
||||
| Docker Engine / Desktop | 24.0 |
|
||||
| Docker Compose | v2 |
|
||||
| Git | 2.40 |
|
||||
| **Node.js** | 22+ |
|
||||
| **Python** | 3.13+ |
|
||||
| GNU Make | 4.4 |
|
||||
| Kind | 0.22 |
|
||||
| Helm | 3.14 |
|
||||
| kubectl | 1.29 |
|
||||
| mkcert | 1.4 |
|
||||
|
||||
|
||||
## 5. Ports (dev defaults)
|
||||
|
||||
| Port | Service |
|
||||
| --------- | --------------------- |
|
||||
| 3000 | Next.js |
|
||||
| 8071 | Django |
|
||||
| 4444 | Y-Provider |
|
||||
| 8080 | Keycloak |
|
||||
| 8083 | Nginx proxy |
|
||||
| 9000/9001 | MinIO |
|
||||
| 15432 | PostgreSQL (main) |
|
||||
| 5433 | PostgreSQL (Keycloak) |
|
||||
| 1081 | MailCatcher |
|
||||
|
||||
## 6. Sizing Guidelines
|
||||
|
||||
**RAM** – start at 8 GB dev / 16 GB staging / 32 GB prod. Postgres and Keycloak are the first to OOM; scale them first.
|
||||
|
||||
> **OS considerations:** Windows systems typically require 10-20% more RAM than Linux due to higher OS overhead. Docker Desktop on Windows also uses additional memory compared to native Linux Docker.
|
||||
|
||||
**CPU** – budget one vCPU per busy container until Celery or Next.js builds saturate.
|
||||
|
||||
**Disk** – SSD; add 10 GB extra for the Docker layer cache.
|
||||
|
||||
**MinIO** – for demos, mount a local folder instead of running MinIO to save 2 GB+ of RAM.
|
||||
@@ -30,4 +30,41 @@ body {
|
||||
|
||||
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 ⬇️:
|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
# **Custom Translations** 📝
|
||||
|
||||
The translations can be partially overridden 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
|
||||
@@ -0,0 +1,194 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
## Line Ending Issues on Windows (LF/CRLF)
|
||||
|
||||
### Problem Description
|
||||
|
||||
This project uses **LF (Line Feed: `\n`) line endings** exclusively. Windows users may encounter issues because:
|
||||
|
||||
- **Windows** defaults to CRLF (Carriage Return + Line Feed: `\r\n`) for line endings
|
||||
- **This project** uses LF line endings for consistency across all platforms
|
||||
- **Git** may automatically convert line endings, causing conflicts or build failures
|
||||
|
||||
### Common Symptoms
|
||||
|
||||
- Git shows files as modified even when no changes were made
|
||||
- Error messages like "warning: LF will be replaced by CRLF"
|
||||
- Build failures or linting errors due to line ending mismatches
|
||||
|
||||
### Solutions for Windows Users
|
||||
|
||||
#### Configure Git to Preserve LF (Recommended)
|
||||
|
||||
Configure Git to NOT convert line endings and preserve LF:
|
||||
|
||||
```bash
|
||||
git config core.autocrlf false
|
||||
git config core.eol lf
|
||||
```
|
||||
|
||||
This tells Git to:
|
||||
- Never convert line endings automatically
|
||||
- Always use LF for line endings in working directory
|
||||
|
||||
|
||||
#### Fix Existing Repository with Wrong Line Endings
|
||||
|
||||
If you already have CRLF line endings in your local repository, the **best approach** is to configure Git properly and clone the project again:
|
||||
|
||||
1. **Configure Git first**:
|
||||
```bash
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
```
|
||||
|
||||
2. **Clone the project fresh** (recommended):
|
||||
```bash
|
||||
# Navigate to parent directory
|
||||
cd ..
|
||||
|
||||
# Remove current repository (backup your changes first!)
|
||||
rm -rf docs
|
||||
|
||||
# Clone again with correct line endings
|
||||
git clone git@github.com:suitenumerique/docs.git
|
||||
```
|
||||
|
||||
**Alternative**: If you have uncommitted changes and cannot re-clone:
|
||||
|
||||
1. **Backup your changes**:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Save changes before fixing line endings"
|
||||
```
|
||||
|
||||
2. **Remove all files from Git's index**:
|
||||
```bash
|
||||
git rm --cached -r .
|
||||
```
|
||||
|
||||
3. **Reset Git configuration** (if not done globally):
|
||||
```bash
|
||||
git config core.autocrlf false
|
||||
git config core.eol lf
|
||||
```
|
||||
|
||||
4. **Re-add all files** (Git will use LF line endings):
|
||||
```bash
|
||||
git add .
|
||||
```
|
||||
|
||||
5. **Commit the changes**:
|
||||
```bash
|
||||
git commit -m "✏️(project) Fix line endings to LF"
|
||||
```
|
||||
|
||||
## Minio Permission Issues on Windows
|
||||
|
||||
### Problem Description
|
||||
|
||||
On Windows, you may encounter permission-related errors when running Minio in development mode with Docker Compose. This typically happens because:
|
||||
|
||||
- **Windows file permissions** don't map well to Unix-style user IDs used in Docker containers
|
||||
- **Docker Desktop** may have issues with user mapping when using the `DOCKER_USER` environment variable
|
||||
- **Minio container** fails to start or access volumes due to permission conflicts
|
||||
|
||||
### Common Symptoms
|
||||
|
||||
- Minio container fails to start with permission denied errors
|
||||
- Error messages related to file system permissions in Minio logs
|
||||
- Unable to create or access buckets in the development environment
|
||||
- Docker Compose showing Minio service as unhealthy or exited
|
||||
|
||||
### Solution for Windows Users
|
||||
|
||||
If you encounter Minio permission issues on Windows, you can temporarily disable user mapping for the Minio service:
|
||||
|
||||
1. **Open the `compose.yml` file**
|
||||
|
||||
2. **Comment out the user directive** in the `minio` service section:
|
||||
```yaml
|
||||
minio:
|
||||
# user: ${DOCKER_USER:-1000} # Comment this line on Windows if permission issues occur
|
||||
image: minio/minio
|
||||
environment:
|
||||
- MINIO_ROOT_USER=impress
|
||||
- MINIO_ROOT_PASSWORD=password
|
||||
# ... rest of the configuration
|
||||
```
|
||||
|
||||
3. **Restart the services**:
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
- Commenting out the `user` directive allows the Minio container to run with its default user
|
||||
- This bypasses Windows-specific permission mapping issues
|
||||
- The container will have the necessary permissions to access and manage the mounted volumes
|
||||
|
||||
### Note
|
||||
|
||||
This is a **development-only workaround**. In production environments, proper user mapping and security considerations should be maintained according to your deployment requirements.
|
||||
|
||||
## Frontend File Watching Issues on Windows
|
||||
|
||||
### Problem Description
|
||||
|
||||
Windows users may experience issues with file watching in the frontend-development container. This typically happens because:
|
||||
|
||||
- **Docker on Windows** has known limitations with file change detection
|
||||
- **Node.js file watchers** may not detect changes properly on Windows filesystem
|
||||
- **Hot reloading** fails to trigger when files are modified
|
||||
|
||||
### Common Symptoms
|
||||
|
||||
- Changes to frontend code aren't detected automatically
|
||||
- Hot module replacement doesn't work as expected
|
||||
- Need to manually restart the frontend container after code changes
|
||||
- Console shows no reaction when saving files
|
||||
|
||||
### Solution: Enable WATCHPACK_POLLING
|
||||
|
||||
Add the `WATCHPACK_POLLING=true` environment variable to the frontend-development service in your local environment:
|
||||
|
||||
1. **Modify the `compose.yml` file** by adding the environment variable to the frontend-development service:
|
||||
|
||||
```yaml
|
||||
frontend-development:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/Dockerfile
|
||||
target: impress-dev
|
||||
args:
|
||||
API_ORIGIN: "http://localhost:8071"
|
||||
PUBLISH_AS_MIT: "false"
|
||||
SW_DEACTIVATED: "true"
|
||||
image: impress:frontend-development
|
||||
environment:
|
||||
- WATCHPACK_POLLING=true # Add this line for Windows users
|
||||
volumes:
|
||||
- ./src/frontend:/home/frontend
|
||||
- /home/frontend/node_modules
|
||||
- /home/frontend/apps/impress/node_modules
|
||||
ports:
|
||||
- "3000:3000"
|
||||
```
|
||||
|
||||
2. **Restart your containers**:
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
- `WATCHPACK_POLLING=true` forces the file watcher to use polling instead of filesystem events
|
||||
- Polling periodically checks for file changes rather than relying on OS-level file events
|
||||
- This is more reliable on Windows but slightly increases CPU usage
|
||||
- Changes to your frontend code should now be detected properly, enabling hot reloading
|
||||
|
||||
### Note
|
||||
|
||||
This setting is primarily needed for Windows users. Linux and macOS users typically don't need this setting as file watching works correctly by default on those platforms.
|
||||
@@ -62,8 +62,5 @@ COLLABORATION_SERVER_ORIGIN=http://localhost:3000
|
||||
COLLABORATION_SERVER_SECRET=my-secret
|
||||
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
|
||||
|
||||
# Frontend
|
||||
FRONTEND_THEME=default
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED=True
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED=True
|
||||
FRONTEND_URL_JSON_FOOTER=http://frontend:3000/contents/footer-demo.json
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
Y_PROVIDER_API_KEY=yprovider-api-key
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# For the CI job test-e2e
|
||||
SUSTAINED_THROTTLE_RATES="200/hour"
|
||||
BURST_THROTTLE_RATES="200/minute"
|
||||
DJANGO_SERVER_TO_SERVER_API_TOKENS=test-e2e
|
||||
Y_PROVIDER_API_KEY=yprovider-api-key
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
SUSTAINED_THROTTLE_RATES="200/hour"
|
||||
|
||||
+7
-9
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": ["github>numerique-gouv/renovate-configuration"],
|
||||
"dependencyDashboard": true,
|
||||
"labels": ["dependencies", "noChangeLog"],
|
||||
"labels": ["dependencies", "noChangeLog", "automated"],
|
||||
"packageRules": [
|
||||
{
|
||||
"enabled": false,
|
||||
@@ -10,20 +10,18 @@
|
||||
"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",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
"""API endpoints"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from urllib.parse import unquote, urlparse
|
||||
from urllib.parse import unquote, urlencode, 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, caches
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import connection, transaction
|
||||
@@ -16,14 +18,14 @@ from django.db import models as db
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Left, Length
|
||||
from django.http import Http404, StreamingHttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.text import capfirst
|
||||
from django.urls import reverse
|
||||
from django.utils.text import capfirst, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
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
|
||||
@@ -32,7 +34,6 @@ from rest_framework.throttling import UserRateThrottle
|
||||
from core import authentication, enums, models
|
||||
from core.services.ai_services import AIService
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.services.config_services import get_footer_json
|
||||
from core.utils import extract_attachments, filter_descendants
|
||||
|
||||
from . import permissions, serializers, utils
|
||||
@@ -403,7 +404,7 @@ class DocumentViewSet(
|
||||
|
||||
Example:
|
||||
- Ascending: GET /api/v1.0/documents/?ordering=created_at
|
||||
- Desceding: GET /api/v1.0/documents/?ordering=-title
|
||||
- Descending: GET /api/v1.0/documents/?ordering=-title
|
||||
|
||||
### Filtering:
|
||||
- `is_creator_me=true`: Returns documents created by the current user.
|
||||
@@ -576,7 +577,7 @@ class DocumentViewSet(
|
||||
queryset, filter_data["is_favorite"]
|
||||
)
|
||||
|
||||
# Apply ordering only now that everyting is filtered and annotated
|
||||
# Apply ordering only now that everything is filtered and annotated
|
||||
queryset = filters.OrderingFilter().filter_queryset(
|
||||
self.request, queryset, self
|
||||
)
|
||||
@@ -630,6 +631,33 @@ class DocumentViewSet(
|
||||
"""Override to implement a soft delete instead of dumping the record in database."""
|
||||
instance.soft_delete()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Check rules about collaboration."""
|
||||
|
||||
shared_cache = caches["shared"]
|
||||
cache_key = f"docs:state:{serializer.instance.id}"
|
||||
doc_state = shared_cache.get(cache_key, enums.DEFAULT_DOCS_STATE.copy())
|
||||
|
||||
session_key = self.request.session.session_key
|
||||
|
||||
if doc_state["wsUsers"] and not session_key in doc_state["wsUsers"]:
|
||||
raise drf.exceptions.PermissionDenied(
|
||||
"You are not allowed to edit this document."
|
||||
)
|
||||
|
||||
if doc_state["httpUser"] and doc_state["httpUser"] != session_key:
|
||||
raise drf.exceptions.PermissionDenied(
|
||||
"You are not allowed to edit this document."
|
||||
)
|
||||
|
||||
if doc_state["httpUser"] is None:
|
||||
doc_state["httpUser"] = session_key
|
||||
shared_cache.set(cache_key, doc_state)
|
||||
|
||||
shared_cache.touch(cache_key)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=False,
|
||||
methods=["get"],
|
||||
@@ -889,7 +917,7 @@ class DocumentViewSet(
|
||||
)
|
||||
|
||||
# Compute cache for ancestors links to avoid many queries while computing
|
||||
# abilties for his documents in the tree!
|
||||
# abilities for his documents in the tree!
|
||||
ancestors_links.append(
|
||||
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
|
||||
)
|
||||
@@ -1156,7 +1184,10 @@ class DocumentViewSet(
|
||||
|
||||
# Prepare metadata for storage
|
||||
extra_args = {
|
||||
"Metadata": {"owner": str(request.user.id)},
|
||||
"Metadata": {
|
||||
"owner": str(request.user.id),
|
||||
"status": enums.DocumentAttachmentStatus.PROCESSING,
|
||||
},
|
||||
"ContentType": serializer.validated_data["content_type"],
|
||||
}
|
||||
file_unsafe = ""
|
||||
@@ -1188,8 +1219,18 @@ 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"{settings.MEDIA_URL:s}{key:s}"},
|
||||
{
|
||||
"file": f"{url:s}?{parameters:s}",
|
||||
},
|
||||
status=drf.status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
@@ -1271,11 +1312,71 @@ 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"],
|
||||
@@ -1711,11 +1812,11 @@ class ConfigView(drf.views.APIView):
|
||||
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_FOOTER_FEATURE_ENABLED",
|
||||
"FRONTEND_THEME",
|
||||
"MEDIA_BASE_URL",
|
||||
"POSTHOG_KEY",
|
||||
@@ -1728,23 +1829,41 @@ 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 {}
|
||||
|
||||
class FooterView(drf.views.APIView):
|
||||
"""API ViewSet for sharing the footer JSON."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@method_decorator(cache_page(settings.FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT))
|
||||
def get(self, request):
|
||||
"""
|
||||
GET /api/v1.0/footer/
|
||||
Return the footer JSON.
|
||||
"""
|
||||
json_footer = (
|
||||
get_footer_json(settings.FRONTEND_URL_JSON_FOOTER)
|
||||
if settings.FRONTEND_URL_JSON_FOOTER
|
||||
else {}
|
||||
cache_key = (
|
||||
f"theme_customization_{slugify(settings.THEME_CUSTOMIZATION_FILE_PATH)}"
|
||||
)
|
||||
return drf.response.Response(json_footer)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Cache utilities"""
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def shared_key_func(key: str, key_prefix: str, version: int = 1) -> str:
|
||||
"""
|
||||
Compute key for shared cache. In order to be compatiable with other system,
|
||||
only the key is used.
|
||||
"""
|
||||
return key
|
||||
@@ -3,6 +3,7 @@ Core application enums declaration
|
||||
"""
|
||||
|
||||
import re
|
||||
from enum import StrEnum
|
||||
|
||||
from django.conf import global_settings, settings
|
||||
from django.db import models
|
||||
@@ -21,6 +22,11 @@ MEDIA_STORAGE_URL_EXTRACT = re.compile(
|
||||
f"{settings.MEDIA_URL:s}({UUID_REGEX}/{ATTACHMENTS_FOLDER}/{UUID_REGEX}{FILE_EXT_REGEX})"
|
||||
)
|
||||
|
||||
DEFAULT_DOCS_STATE = {
|
||||
"httpUser": None,
|
||||
"wsUsers": [],
|
||||
}
|
||||
|
||||
|
||||
# In Django's code base, `LANGUAGES` is set by default with all supported languages.
|
||||
# We can use it for the choice of languages which should not be limited to the few languages
|
||||
@@ -38,3 +44,10 @@ 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"
|
||||
|
||||
@@ -35,6 +35,8 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
# Skip postgeneration save, no save is made in the postgeneration methods.
|
||||
skip_postgeneration_save = True
|
||||
|
||||
sub = factory.Sequence(lambda n: f"user{n!s}")
|
||||
email = factory.Faker("email")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Force session creation for all requests."""
|
||||
|
||||
|
||||
class ForceSessionMiddleware:
|
||||
"""
|
||||
Force session creation for unauthenticated users.
|
||||
Must be used after Authentication middleware.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
"""Initialize the middleware."""
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
"""Force session creation for unauthenticated users."""
|
||||
|
||||
if not request.user.is_authenticated and request.session.session_key is None:
|
||||
request.session.save()
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -504,7 +504,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddConstraint(
|
||||
model_name="documentaccess",
|
||||
constraint=models.CheckConstraint(
|
||||
check=models.Q(
|
||||
condition=models.Q(
|
||||
models.Q(("team", ""), ("user__isnull", False)),
|
||||
models.Q(("team__gt", ""), ("user__isnull", True)),
|
||||
_connector="OR",
|
||||
@@ -540,7 +540,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddConstraint(
|
||||
model_name="templateaccess",
|
||||
constraint=models.CheckConstraint(
|
||||
check=models.Q(
|
||||
condition=models.Q(
|
||||
models.Q(("team", ""), ("user__isnull", False)),
|
||||
models.Q(("team__gt", ""), ("user__isnull", True)),
|
||||
_connector="OR",
|
||||
|
||||
@@ -520,7 +520,7 @@ class Document(MP_Node, BaseModel):
|
||||
verbose_name_plural = _("Documents")
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
check=(
|
||||
condition=(
|
||||
models.Q(deleted_at__isnull=True)
|
||||
| models.Q(deleted_at=models.F("ancestors_deleted_at"))
|
||||
),
|
||||
@@ -747,7 +747,7 @@ class Document(MP_Node, BaseModel):
|
||||
for ancestor in ancestors_links:
|
||||
links_definitions[ancestor["link_reach"]].add(ancestor["link_role"])
|
||||
|
||||
return dict(links_definitions) # Convert defaultdict back to a normal dict
|
||||
return dict(links_definitions) # Convert default dict back to a normal dict
|
||||
|
||||
def compute_ancestors_links(self, user):
|
||||
"""
|
||||
@@ -835,6 +835,7 @@ 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,
|
||||
@@ -1087,7 +1088,7 @@ class DocumentAccess(BaseAccess):
|
||||
violation_error_message=_("This team is already in this document."),
|
||||
),
|
||||
models.CheckConstraint(
|
||||
check=models.Q(user__isnull=False, team="")
|
||||
condition=models.Q(user__isnull=False, team="")
|
||||
| models.Q(user__isnull=True, team__gt=""),
|
||||
name="check_document_access_either_user_or_team",
|
||||
violation_error_message=_("Either user or team must be set, not both."),
|
||||
@@ -1235,7 +1236,7 @@ class TemplateAccess(BaseAccess):
|
||||
violation_error_message=_("This team is already in this template."),
|
||||
),
|
||||
models.CheckConstraint(
|
||||
check=models.Q(user__isnull=False, team="")
|
||||
condition=models.Q(user__isnull=False, team="")
|
||||
| models.Q(user__isnull=True, team__gt=""),
|
||||
name="check_template_access_either_user_or_team",
|
||||
violation_error_message=_("Either user or team must be set, not both."),
|
||||
|
||||
@@ -44,7 +44,7 @@ AI_ACTIONS = {
|
||||
}
|
||||
|
||||
AI_TRANSLATE = (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure 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."
|
||||
|
||||
@@ -17,7 +17,7 @@ class CollaborationService:
|
||||
def reset_connections(self, room, user_id=None):
|
||||
"""
|
||||
Reset connections of a room in the collaboration server.
|
||||
Reseting a connection means that the user will be disconnected and will
|
||||
Resetting a connection means that the user will be disconnected and will
|
||||
have to reconnect to the collaboration server, with updated rights.
|
||||
"""
|
||||
endpoint = "reset-connections"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Config services."""
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_footer_json(footer_json_url: str) -> dict:
|
||||
"""
|
||||
Fetches the footer JSON from the given URL."
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
footer_json_url, timeout=5, headers={"User-Agent": "Docs-Application"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
footer_json = response.json()
|
||||
|
||||
return footer_json
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
logger.error("Failed to fetch footer JSON: %s", e)
|
||||
return {}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.cache import caches
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -14,7 +14,8 @@ VIA = [USER, TEAM]
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_cache():
|
||||
"""Fixture to clear the cache before each test."""
|
||||
cache.clear()
|
||||
for cache in caches.all():
|
||||
cache.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -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 exising identity in the db
|
||||
# Build an invitation to the email of an existing identity in the db
|
||||
invitation_values = {
|
||||
"email": existing_user.email,
|
||||
"role": random.choice(models.RoleChoices.values),
|
||||
|
||||
@@ -150,7 +150,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):
|
||||
"""
|
||||
Autenticated who are not related to a document should be able to request AI transform
|
||||
Authenticated 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()
|
||||
|
||||
@@ -99,7 +99,7 @@ def test_api_documents_ai_translate_anonymous_success(mock_create):
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure 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 +172,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):
|
||||
"""
|
||||
Autenticated who are not related to a document should be able to request AI translate
|
||||
Authenticated 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 +197,7 @@ def test_api_documents_ai_translate_authenticated_success(mock_create, reach, ro
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure 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 +274,7 @@ def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_te
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Keep the same html stucture and formatting. "
|
||||
"Keep the same html structure and formatting. "
|
||||
"Translate the content in the html to the "
|
||||
"specified language Colombian Spanish. "
|
||||
"Check the translation for accuracy and make any necessary corrections. "
|
||||
|
||||
@@ -4,6 +4,8 @@ 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
|
||||
@@ -12,6 +14,7 @@ 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
|
||||
@@ -59,12 +62,17 @@ 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/"
|
||||
response = APIClient().post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = APIClient().post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
|
||||
file_path = response.json()["file"]
|
||||
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)
|
||||
file_id = match.group(1)
|
||||
# Validate that file_id is a valid UUID
|
||||
@@ -74,12 +82,13 @@ 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", "")
|
||||
key = file_path.replace("/media/", "")
|
||||
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=key
|
||||
)
|
||||
|
||||
assert file_head["Metadata"] == {"owner": "None"}
|
||||
assert file_head["Metadata"] == {"owner": "None", "status": "processing"}
|
||||
assert file_head["ContentType"] == "image/png"
|
||||
assert file_head["ContentDisposition"] == 'inline; filename="test.png"'
|
||||
|
||||
@@ -127,7 +136,7 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
|
||||
)
|
||||
def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||
"""
|
||||
Autenticated users who are not related to a document should be able to upload
|
||||
Authenticated 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()
|
||||
@@ -139,14 +148,24 @@ 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/"
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
|
||||
match = pattern.search(response.json()["file"])
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -210,12 +229,17 @@ 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/"
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
file_path = response.json()["file"]
|
||||
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
|
||||
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)
|
||||
file_id = match.group(1)
|
||||
|
||||
@@ -226,11 +250,12 @@ 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", "")
|
||||
key = file_path.replace("/media/", "")
|
||||
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
|
||||
file_head = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=key
|
||||
)
|
||||
assert file_head["Metadata"] == {"owner": str(user.id)}
|
||||
assert file_head["Metadata"] == {"owner": str(user.id), "status": "processing"}
|
||||
assert file_head["ContentType"] == "image/png"
|
||||
assert file_head["ContentDisposition"] == 'inline; filename="test.png"'
|
||||
|
||||
@@ -255,7 +280,7 @@ def test_api_documents_attachment_upload_invalid(client):
|
||||
|
||||
|
||||
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
|
||||
"""The uploaded file should not exceeed the maximum size in settings."""
|
||||
"""The uploaded file should not exceed the maximum size in settings."""
|
||||
settings.DOCUMENT_IMAGE_MAX_SIZE = 1048576 # 1 MB for test
|
||||
|
||||
user = factories.UserFactory()
|
||||
@@ -304,12 +329,18 @@ 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)
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
file_path = response.json()["file"]
|
||||
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.{extension:s}")
|
||||
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]
|
||||
|
||||
match = pattern.search(file_path)
|
||||
file_id = match.group(1)
|
||||
|
||||
@@ -324,11 +355,16 @@ 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", "")
|
||||
key = file_path.replace("/media/", "")
|
||||
mock_analyse_file.assert_called_once_with(key, document_id=document.id)
|
||||
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"}
|
||||
assert file_head["Metadata"] == {
|
||||
"owner": str(user.id),
|
||||
"is_unsafe": "true",
|
||||
"status": "processing",
|
||||
}
|
||||
assert file_head["ContentType"] == content_type
|
||||
assert file_head["ContentDisposition"] == f'attachment; filename="{name:s}"'
|
||||
|
||||
@@ -364,12 +400,17 @@ def test_api_documents_attachment_upload_unsafe():
|
||||
file = SimpleUploadedFile(
|
||||
name="script.exe", content=b"\x4d\x5a\x90\x00\x03\x00\x00\x00"
|
||||
)
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
|
||||
response = client.post(url, {"file": file}, format="multipart")
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
file_path = response.json()["file"]
|
||||
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.exe")
|
||||
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]
|
||||
match = pattern.search(file_path)
|
||||
file_id = match.group(1)
|
||||
|
||||
@@ -381,11 +422,20 @@ 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"}
|
||||
assert file_head["ContentType"] == "application/octet-stream"
|
||||
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["ContentDisposition"] == 'attachment; filename="script.exe"'
|
||||
|
||||
@@ -279,7 +279,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 usefull if an OIDC provider modifies
|
||||
happen in a healthy OIDC federation but can be useful if an OIDC provider modifies
|
||||
users sub on each login for example...
|
||||
"""
|
||||
user = factories.UserFactory(language="en-us")
|
||||
|
||||
@@ -15,6 +15,7 @@ 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
|
||||
@@ -45,6 +46,7 @@ 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])
|
||||
@@ -93,7 +95,15 @@ def test_api_documents_media_auth_extensions():
|
||||
keys = []
|
||||
for ext in extensions:
|
||||
filename = f"{uuid4()!s}.{ext:s}"
|
||||
keys.append(f"{document_id!s}/attachments/{filename: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)
|
||||
|
||||
factories.DocumentFactory(link_reach="public", attachments=keys)
|
||||
|
||||
@@ -142,6 +152,7 @@ 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")
|
||||
@@ -205,6 +216,7 @@ 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])
|
||||
@@ -283,6 +295,7 @@ 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(
|
||||
@@ -321,3 +334,70 @@ 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"
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
"""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}",
|
||||
}
|
||||
@@ -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 rigths on its parent.
|
||||
if the user has no rights on its parent.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
|
||||
@@ -48,6 +48,7 @@ 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,
|
||||
@@ -111,6 +112,7 @@ 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,
|
||||
@@ -210,6 +212,7 @@ 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,
|
||||
@@ -279,8 +282,9 @@ 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),
|
||||
"move": False,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
"partial_update": grand_parent.link_role == "editor",
|
||||
"restore": False,
|
||||
"retrieve": True,
|
||||
@@ -460,6 +464,7 @@ 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",
|
||||
|
||||
@@ -91,6 +91,7 @@ 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,
|
||||
|
||||
@@ -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" ot the document if these
|
||||
updated content should be added to the list of "attachments" to the document if these
|
||||
attachments are already readable by anonymous users.
|
||||
"""
|
||||
image_keys = [f"{uuid4()!s}/attachments/{uuid4()!s}.png" for _ in range(4)]
|
||||
@@ -47,7 +47,7 @@ def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_qu
|
||||
factories.DocumentFactory(attachments=[image_keys[3]], link_reach="restricted")
|
||||
expected_keys = {image_keys[i] for i in [0, 1]}
|
||||
|
||||
with django_assert_num_queries(9):
|
||||
with django_assert_num_queries(11):
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys)},
|
||||
@@ -60,7 +60,7 @@ def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_qu
|
||||
|
||||
# Check that the db query to check attachments readability for extracted
|
||||
# keys is not done if the content changes but no new keys are found
|
||||
with django_assert_num_queries(5):
|
||||
with django_assert_num_queries(7):
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys[:2])},
|
||||
@@ -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" ot the document if these
|
||||
updated content should be added to the list of "attachments" to the document if these
|
||||
attachments are already readable by the editing user.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
@@ -98,7 +98,7 @@ def test_api_documents_update_new_attachment_keys_authenticated(
|
||||
factories.DocumentFactory(attachments=[image_keys[4]], users=[user])
|
||||
expected_keys = {image_keys[i] for i in [0, 1, 2, 4]}
|
||||
|
||||
with django_assert_num_queries(10):
|
||||
with django_assert_num_queries(12):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys)},
|
||||
@@ -111,7 +111,7 @@ def test_api_documents_update_new_attachment_keys_authenticated(
|
||||
|
||||
# Check that the db query to check attachments readability for extracted
|
||||
# keys is not done if the content changes but no new keys are found
|
||||
with django_assert_num_queries(6):
|
||||
with django_assert_num_queries(8):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys[:2])},
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Test config API endpoints in the Impress core app.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.test import override_settings
|
||||
|
||||
import pytest
|
||||
@@ -16,15 +18,16 @@ 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_HOMEPAGE_FEATURE_ENABLED=True,
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED=True,
|
||||
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):
|
||||
@@ -39,11 +42,11 @@ 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_FOOTER_FEATURE_ENABLED": True,
|
||||
"FRONTEND_THEME": "test-theme",
|
||||
"LANGUAGES": [
|
||||
["en-us", "English"],
|
||||
@@ -57,4 +60,98 @@ def test_api_config(is_authenticated):
|
||||
"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
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Test the footer API."""
|
||||
|
||||
import responses
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
def test_api_footer_without_settings_configured(settings):
|
||||
"""Test the footer API without settings configured."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = None
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {}
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_invalid_request(settings):
|
||||
"""Test the footer API with an invalid request."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://invalid-request.com"
|
||||
|
||||
footer_response = responses.get(settings.FRONTEND_URL_JSON_FOOTER, status=404)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_invalid_json(settings):
|
||||
"""Test the footer API with an invalid JSON response."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
|
||||
|
||||
footer_response = responses.get(
|
||||
settings.FRONTEND_URL_JSON_FOOTER, status=200, body="invalid json"
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_valid_json(settings):
|
||||
"""Test the footer API with an invalid JSON response."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
|
||||
|
||||
footer_response = responses.get(
|
||||
settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"}
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"foo": "bar"}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_footer_with_valid_json_and_cache(settings):
|
||||
"""Test the footer API with an invalid JSON response."""
|
||||
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
|
||||
|
||||
footer_response = responses.get(
|
||||
settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"}
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"foo": "bar"}
|
||||
assert footer_response.call_count == 1
|
||||
|
||||
response = client.get("/api/v1.0/footer/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"foo": "bar"}
|
||||
# The cache should have been used
|
||||
assert footer_response.call_count == 1
|
||||
@@ -0,0 +1,76 @@
|
||||
"""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)
|
||||
@@ -165,6 +165,7 @@ 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": {
|
||||
@@ -231,6 +232,7 @@ def test_models_documents_get_abilities_reader(
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
"partial_update": False,
|
||||
"restore": False,
|
||||
@@ -293,6 +295,7 @@ def test_models_documents_get_abilities_editor(
|
||||
"restricted": ["reader", "editor"],
|
||||
},
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
"partial_update": True,
|
||||
"restore": False,
|
||||
@@ -344,6 +347,7 @@ 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,
|
||||
@@ -392,6 +396,7 @@ 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,
|
||||
@@ -443,6 +448,7 @@ 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,
|
||||
@@ -501,6 +507,7 @@ 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,
|
||||
@@ -557,6 +564,7 @@ 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,
|
||||
@@ -1056,7 +1064,7 @@ def test_models_documents_restore(django_assert_num_queries):
|
||||
assert document.deleted_at is not None
|
||||
assert document.ancestors_deleted_at == document.deleted_at
|
||||
|
||||
with django_assert_num_queries(8):
|
||||
with django_assert_num_queries(10):
|
||||
document.restore()
|
||||
document.refresh_from_db()
|
||||
assert document.deleted_at is None
|
||||
@@ -1099,7 +1107,7 @@ def test_models_documents_restore_complex(django_assert_num_queries):
|
||||
assert child2.ancestors_deleted_at == document.deleted_at
|
||||
|
||||
# Restore the item
|
||||
with django_assert_num_queries(11):
|
||||
with django_assert_num_queries(13):
|
||||
document.restore()
|
||||
document.refresh_from_db()
|
||||
child1.refresh_from_db()
|
||||
@@ -1149,7 +1157,7 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
|
||||
# Restoring the grand parent should not restore the document
|
||||
# as it was deleted before the grand parent
|
||||
with django_assert_num_queries(9):
|
||||
with django_assert_num_queries(11):
|
||||
grand_parent.restore()
|
||||
|
||||
grand_parent.refresh_from_db()
|
||||
|
||||
@@ -56,5 +56,4 @@ urlpatterns = [
|
||||
),
|
||||
),
|
||||
path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()),
|
||||
path(f"api/{settings.API_VERSION}/footer/", viewsets.FooterView.as_view()),
|
||||
]
|
||||
|
||||
@@ -8,11 +8,11 @@ NB_OBJECTS = {
|
||||
|
||||
DEV_USERS = [
|
||||
{"username": "impress", "email": "impress@impress.world", "language": "en-us"},
|
||||
{"username": "user-e2e-webkit", "email": "user@webkit.e2e", "language": "en-us"},
|
||||
{"username": "user-e2e-firefox", "email": "user@firefox.e2e", "language": "en-us"},
|
||||
{"username": "user-e2e-webkit", "email": "user@webkit.test", "language": "en-us"},
|
||||
{"username": "user-e2e-firefox", "email": "user@firefox.test", "language": "en-us"},
|
||||
{
|
||||
"username": "user-e2e-chromium",
|
||||
"email": "user@chromium.e2e",
|
||||
"email": "user@chromium.test",
|
||||
"language": "en-us",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -33,9 +33,9 @@ def test_commands_create_demo():
|
||||
# assert dev users have doc accesses
|
||||
user = models.User.objects.get(email="impress@impress.world")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@webkit.e2e")
|
||||
user = models.User.objects.get(email="user@webkit.test")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@firefox.e2e")
|
||||
user = models.User.objects.get(email="user@firefox.test")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@chromium.e2e")
|
||||
user = models.User.objects.get(email="user@chromium.test")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Impress package. Import the celery app early to load shared task form dependencies."""
|
||||
|
||||
from .celery_app import app as celery_app
|
||||
|
||||
__all__ = ["celery_app"]
|
||||
|
||||
@@ -11,6 +11,9 @@ 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
|
||||
@@ -20,4 +23,4 @@ app = Celery("impress")
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
|
||||
# Load task modules from all registered Django apps.
|
||||
app.autodiscover_tasks()
|
||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
-20
@@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import os
|
||||
import tomllib
|
||||
@@ -283,6 +284,7 @@ class Base(Configuration):
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"core.middleware.ForceSessionMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"dockerflow.django.middleware.DockerflowMiddleware",
|
||||
]
|
||||
@@ -317,11 +319,30 @@ class Base(Configuration):
|
||||
"django.contrib.staticfiles",
|
||||
# OIDC third party
|
||||
"mozilla_django_oidc",
|
||||
"lasuite.malware_detection",
|
||||
]
|
||||
|
||||
# Cache
|
||||
CACHES = {
|
||||
"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
|
||||
"shared": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": values.Value(
|
||||
"redis://redis:6379/0",
|
||||
environ_name="REDIS_URL",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"TIMEOUT": values.IntegerValue(
|
||||
120, # timeout in seconds
|
||||
environ_name="SHARED_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"SERIALIZER": "django_redis.serializers.json.JSONSerializer",
|
||||
},
|
||||
"KEY_FUNCTION": "core.cache.shared_key_func",
|
||||
},
|
||||
}
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
@@ -395,7 +416,7 @@ class Base(Configuration):
|
||||
|
||||
# CORS
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(True)
|
||||
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False)
|
||||
CORS_ALLOWED_ORIGINS = values.ListValue([])
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
|
||||
|
||||
@@ -412,33 +433,37 @@ 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=False,
|
||||
default=True,
|
||||
environ_name="FRONTEND_HOMEPAGE_FEATURE_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
FRONTEND_URL_JSON_FOOTER = values.Value(
|
||||
None, environ_name="FRONTEND_URL_JSON_FOOTER", environ_prefix=None
|
||||
)
|
||||
FRONTEND_FOOTER_FEATURE_ENABLED = values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="FRONTEND_FOOTER_FEATURE_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT = values.Value(
|
||||
60 * 60 * 24,
|
||||
environ_name="FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT",
|
||||
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(
|
||||
None, environ_name="POSTHOG_KEY", environ_prefix=None
|
||||
@@ -462,7 +487,10 @@ class Base(Configuration):
|
||||
# Session
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
SESSION_CACHE_ALIAS = "default"
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 12
|
||||
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
|
||||
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
|
||||
)
|
||||
SESSION_COOKIE_NAME = "docs_sessionid"
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_CREATE_USER = values.BooleanValue(
|
||||
@@ -677,9 +705,33 @@ 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",
|
||||
@@ -780,8 +832,6 @@ class Development(Base):
|
||||
CSRF_TRUSTED_ORIGINS = ["http://localhost:8072", "http://localhost:3000"]
|
||||
DEBUG = True
|
||||
|
||||
SESSION_COOKIE_NAME = "impress_sessionid"
|
||||
|
||||
USE_SWAGGER = True
|
||||
SESSION_CACHE_ALIAS = "session"
|
||||
CACHES = {
|
||||
@@ -791,7 +841,7 @@ class Development(Base):
|
||||
"session": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": values.Value(
|
||||
"redis://redis:6379/2",
|
||||
"redis://redis:6379/0",
|
||||
environ_name="REDIS_URL",
|
||||
environ_prefix=None,
|
||||
),
|
||||
@@ -804,6 +854,24 @@ class Development(Base):
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
},
|
||||
"shared": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": values.Value(
|
||||
"redis://redis:6379/0",
|
||||
environ_name="REDIS_URL",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"TIMEOUT": values.IntegerValue(
|
||||
120, # timeout in seconds
|
||||
environ_name="SHARED_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"SERIALIZER": "django_redis.serializers.json.JSONSerializer",
|
||||
},
|
||||
"KEY_FUNCTION": "core.cache.shared_key_func",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
@@ -818,6 +886,9 @@ class Test(Base):
|
||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||
]
|
||||
USE_SWAGGER = True
|
||||
# Static files are not used in the test environment
|
||||
# Tests are raising warnings because the /data/static directory does not exist
|
||||
STATIC_ROOT = None
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True)
|
||||
|
||||
@@ -886,7 +957,7 @@ class Production(Base):
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": values.Value(
|
||||
"redis://redis:6379/1",
|
||||
"redis://redis:6379/0",
|
||||
environ_name="REDIS_URL",
|
||||
environ_prefix=None,
|
||||
),
|
||||
@@ -898,6 +969,29 @@ class Production(Base):
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
"KEY_PREFIX": values.Value(
|
||||
"docs",
|
||||
environ_name="CACHES_KEY_PREFIX",
|
||||
environ_prefix=None,
|
||||
),
|
||||
},
|
||||
"shared": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": values.Value(
|
||||
"redis://redis:6379/0",
|
||||
environ_name="REDIS_URL",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"TIMEOUT": values.IntegerValue(
|
||||
120, # timeout in seconds
|
||||
environ_name="SHARED_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"SERIALIZER": "django_redis.serializers.json.JSONSerializer",
|
||||
},
|
||||
"KEY_FUNCTION": "core.cache.shared_key_func",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr "Doare korf"
|
||||
msgid "Format"
|
||||
msgstr "Stumm"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Bugel kentañ"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "Bugel diwezhañ"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Kleiz"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Dehoù"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "implijer"
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
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
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "publik"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "Patrom"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "Patromoù"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -66,32 +66,32 @@ msgstr "Typ"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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/enums.py:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Erstes Unterelement"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "Letztes Unterelement"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr "Erstes Nebenelement"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr "Letztes Nebenelement"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Links"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Rechts"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "Benutzer"
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:907 core/models.py:907
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:911 core/models.py:911
|
||||
#: 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} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
|
||||
|
||||
#: build/lib/core/models.py:917 core/models.py:917
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#, 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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 "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
|
||||
|
||||
#: build/lib/core/models.py:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
|
||||
#: core/models.py:1093 core/models.py:1241
|
||||
#: 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 "Benutzer oder Team müssen gesetzt werden, nicht beides."
|
||||
|
||||
#: build/lib/core/models.py:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr "Code"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: build/lib/core/models.py:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -66,32 +66,32 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
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
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr "Tipo de Cuerpo"
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Primer nodo"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "Último nodo"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr "Primera relación"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr "Última relación"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Izquierda"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Derecha"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "usuario"
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: 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"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: 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:1016 core/models.py:1016
|
||||
#: 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:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: 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:1087 core/models.py:1087
|
||||
#: 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:1093 build/lib/core/models.py:1241
|
||||
#: core/models.py:1093 core/models.py:1241
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr "código"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "público"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "Plantillas"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relación plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relaciones plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: 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:1235 core/models.py:1235
|
||||
#: 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:1258 core/models.py:1258
|
||||
#: 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:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: 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."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 09:05\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: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -66,32 +66,32 @@ msgstr "Type de corps"
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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/enums.py:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Premier enfant"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "Dernier enfant"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr "Premier frère ou sœur"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr "Dernière relation"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Gauche"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Droite"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "utilisateur"
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:907 core/models.py:907
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:911 core/models.py:911
|
||||
#: 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} vous a invité avec le rôle \"{role}\" sur le document suivant :"
|
||||
|
||||
#: build/lib/core/models.py:917 core/models.py:917
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} a partagé un document avec vous : {title}"
|
||||
|
||||
#: build/lib/core/models.py:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Trace du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Traces du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
|
||||
|
||||
#: build/lib/core/models.py:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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 "Ce document est déjà un favori de cet utilisateur."
|
||||
|
||||
#: build/lib/core/models.py:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: 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:1087 core/models.py:1087
|
||||
#: 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:1093 build/lib/core/models.py:1241
|
||||
#: core/models.py:1093 core/models.py:1241
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "public"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "Modèles"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relation modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relations modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Cet utilisateur est déjà dans ce modèle."
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Cette équipe est déjà modèle."
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Sinistra"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Destra"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "utente"
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: 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:1087 core/models.py:1087
|
||||
#: 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:1093 build/lib/core/models.py:1241
|
||||
#: core/models.py:1093 core/models.py:1241
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "pubblico"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "Modello"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "Modelli"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: 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:1235 core/models.py:1235
|
||||
#: 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:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: 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."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -66,32 +66,32 @@ msgstr "Text type"
|
||||
msgid "Format"
|
||||
msgstr "Formaat"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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/enums.py:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Eerste node"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "Laatste node"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr "Eerste naaste"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr "Laatste naaste"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Links"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Rechts"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "gebruiker"
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:907 core/models.py:907
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:911 core/models.py:911
|
||||
#: 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} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
|
||||
|
||||
#: build/lib/core/models.py:917 core/models.py:917
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#, 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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 "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr "De gebruiker is al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Het team is al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1093 build/lib/core/models.py:1241
|
||||
#: core/models.py:1093 core/models.py:1241
|
||||
#: 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 "Een gebruiker of team moet gekozen worden, maar niet beide."
|
||||
|
||||
#: build/lib/core/models.py:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr "omschrijving"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "publiek"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "Template"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "Templates"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr "Template/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr "Template/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr "De gebruiker bestaat al in dit template."
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Het team bestaat al in dit template."
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr "email adres"
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
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
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr "Vrsta telesa"
|
||||
msgid "Format"
|
||||
msgstr "Oblika"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Prvi otrok"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "Zadnji otrok"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr "Prvi brat in sestra"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr "Zadnji brat in sestra"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "Levo"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "Desno"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "uporabnik"
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: 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:1016 core/models.py:1016
|
||||
#: 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:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: 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:1087 core/models.py:1087
|
||||
#: 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:1093 build/lib/core/models.py:1241
|
||||
#: core/models.py:1093 core/models.py:1241
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr "koda"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "javno"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "Predloga"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "Predloge"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr "Odnos predloga/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: 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:1229 core/models.py:1229
|
||||
#: 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:1235 core/models.py:1235
|
||||
#: 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:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: 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."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
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
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: 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."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr ""
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
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
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-04-29 11:48+0000\n"
|
||||
"PO-Revision-Date: 2025-05-05 07:07\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"
|
||||
@@ -66,32 +66,32 @@ msgstr "正文类型"
|
||||
msgid "Format"
|
||||
msgstr "格式"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:966 core/api/viewsets.py:966
|
||||
#: 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:35 core/enums.py:35
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "第一个子项"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
msgid "Last child"
|
||||
msgstr "最后一个子项"
|
||||
|
||||
#: build/lib/core/enums.py:37 core/enums.py:37
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr "第一个同级项"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr "最后一个同级项"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
msgstr "左"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
#: build/lib/core/enums.py:41 core/enums.py:41
|
||||
msgid "Right"
|
||||
msgstr "右"
|
||||
|
||||
@@ -225,8 +225,8 @@ msgstr "用户"
|
||||
msgid "users"
|
||||
msgstr "个用户"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1154
|
||||
#: core/models.py:470 core/models.py:1154
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
msgid "title"
|
||||
msgstr "标题"
|
||||
|
||||
@@ -242,128 +242,128 @@ msgstr "文档"
|
||||
msgid "Documents"
|
||||
msgstr "个文档"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:872 core/models.py:532
|
||||
#: core/models.py:872
|
||||
#: 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:907 core/models.py:907
|
||||
#: 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:911 core/models.py:911
|
||||
#: 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:917 core/models.py:917
|
||||
#: 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:1015 core/models.py:1015
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
msgid "Document/user link traces"
|
||||
msgstr "个文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:1022 core/models.py:1022
|
||||
#: 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:1045 core/models.py:1045
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Document favorite"
|
||||
msgstr "文档收藏"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Document favorites"
|
||||
msgstr "文档收藏夹"
|
||||
|
||||
#: build/lib/core/models.py:1052 core/models.py:1052
|
||||
#: 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:1074 core/models.py:1074
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
msgid "Document/user relation"
|
||||
msgstr "文档/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
msgid "Document/user relations"
|
||||
msgstr "文档/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1081 core/models.py:1081
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
msgid "This user is already in this document."
|
||||
msgstr "该用户已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1087 core/models.py:1087
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
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
|
||||
#: 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:1155 core/models.py:1155
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
msgid "description"
|
||||
msgstr "说明"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
msgid "code"
|
||||
msgstr "代码"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1159 core/models.py:1159
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
msgid "public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:1161 core/models.py:1161
|
||||
#: 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:1167 core/models.py:1167
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
msgid "Template"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
msgid "Templates"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1222 core/models.py:1222
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
msgid "Template/user relation"
|
||||
msgstr "模板/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
msgid "Template/user relations"
|
||||
msgstr "模板/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1229 core/models.py:1229
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
msgid "This user is already in this template."
|
||||
msgstr "该用户已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1235 core/models.py:1235
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
msgid "This team is already in this template."
|
||||
msgstr "该团队已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1258 core/models.py:1258
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
msgid "email address"
|
||||
msgstr "电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
msgid "Document invitation"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
msgid "Document invitations"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1298 core/models.py:1298
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "此电子邮件已经与现有注册用户关联。"
|
||||
|
||||
|
||||
+24
-24
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "3.1.0"
|
||||
version = "3.3.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,21 +25,20 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"beautifulsoup4==4.13.3",
|
||||
"boto3==1.37.33",
|
||||
"beautifulsoup4==4.13.4",
|
||||
"boto3==1.38.36",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.5.1",
|
||||
"celery[redis]==5.5.3",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-filter==25.1",
|
||||
"django-lasuite==0.0.7",
|
||||
"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-timezone-field>=5.1",
|
||||
"django==5.1.8",
|
||||
"django==5.2.3",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
@@ -47,18 +46,19 @@ dependencies = [
|
||||
"easy_thumbnails==2.10",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.23.0",
|
||||
"lxml==5.3.2",
|
||||
"jsonschema==4.24.0",
|
||||
"lxml==5.4.0",
|
||||
"markdown==3.8",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.73.0",
|
||||
"psycopg[binary]==3.2.6",
|
||||
"pycrdt==0.12.12",
|
||||
"openai==1.86.0",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"pycrdt==0.12.21",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk==2.25.1",
|
||||
"redis<6.0.0",
|
||||
"requests==2.32.4",
|
||||
"sentry-sdk==2.30.0",
|
||||
"whitenoise==6.9.0",
|
||||
]
|
||||
|
||||
@@ -71,22 +71,22 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"django-test-migrations==1.4.0",
|
||||
"drf-spectacular-sidecar==2025.4.1",
|
||||
"freezegun==1.5.1",
|
||||
"django-test-migrations==1.5.0",
|
||||
"drf-spectacular-sidecar==2025.6.1",
|
||||
"freezegun==1.5.2",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.1.0",
|
||||
"ipython==9.3.0",
|
||||
"pyfakefs==5.8.0",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.6",
|
||||
"pytest-cov==6.1.1",
|
||||
"pylint==3.3.7",
|
||||
"pytest-cov==6.2.1",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==8.3.5",
|
||||
"pytest==8.4.0",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.6.1",
|
||||
"pytest-xdist==3.7.0",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.11.5",
|
||||
"types-requests==2.32.0.20250328",
|
||||
"ruff==0.11.13",
|
||||
"types-requests==2.32.4.20250611",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
+10
-2
@@ -1,4 +1,9 @@
|
||||
FROM node:20-alpine AS frontend-deps
|
||||
FROM node:24-alpine AS frontend-deps
|
||||
|
||||
# Upgrade system packages to install security updates
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
@@ -39,10 +44,13 @@ 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:1.26-alpine AS frontend-production
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
|
||||
@@ -10,8 +10,6 @@ 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.',
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100">
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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: 292 B After Width: | Height: | Size: 336 B |
@@ -4,11 +4,11 @@ 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_FOOTER_FEATURE_ENABLED: true,
|
||||
FRONTEND_THEME: 'default',
|
||||
FRONTEND_THEME: null,
|
||||
MEDIA_BASE_URL: 'http://localhost:8083',
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
@@ -20,7 +20,26 @@ export const CONFIG = {
|
||||
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,
|
||||
@@ -28,10 +47,7 @@ export const keyCloakSignIn = async (
|
||||
fromHome: boolean = true,
|
||||
) => {
|
||||
if (fromHome) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Proconnect Login' })
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Start Writing' }).first().click();
|
||||
}
|
||||
|
||||
const login = `user-e2e-${browserName}`;
|
||||
@@ -91,9 +107,16 @@ export const createDoc = async (
|
||||
};
|
||||
|
||||
export const verifyDocName = async (page: Page, docName: string) => {
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(
|
||||
page.getByLabel('It is the card information about the document.'),
|
||||
).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(input).toHaveText(docName);
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'doc title input' }),
|
||||
).toHaveText(docName);
|
||||
} catch {
|
||||
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
|
||||
}
|
||||
@@ -102,7 +125,7 @@ export const verifyDocName = async (page: Page, docName: string) => {
|
||||
export const addNewMember = async (
|
||||
page: Page,
|
||||
index: number,
|
||||
role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
|
||||
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
|
||||
fillText: string = 'user ',
|
||||
) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
|
||||
@@ -2,38 +2,14 @@ import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { CONFIG, createDoc } from './common';
|
||||
import { CONFIG, createDoc, overrideConfig } from './common';
|
||||
|
||||
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 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();
|
||||
}
|
||||
await overrideConfig(page, {
|
||||
SENTRY_DSN: 'https://sentry.io/123',
|
||||
});
|
||||
|
||||
const invalidMsg = 'Invalid Sentry Dsn: https://sentry.io/123';
|
||||
@@ -98,18 +74,8 @@ test.describe('Config', () => {
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...CONFIG,
|
||||
AI_FEATURE_ENABLED: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await overrideConfig(page, {
|
||||
AI_FEATURE_ENABLED: false,
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
@@ -129,18 +95,8 @@ test.describe('Config', () => {
|
||||
test('it checks that Crisp is trying to init from config endpoint', 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,
|
||||
CRISP_WEBSITE_ID: '1234',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await overrideConfig(page, {
|
||||
CRISP_WEBSITE_ID: '1234',
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
@@ -151,18 +107,8 @@ test.describe('Config', () => {
|
||||
});
|
||||
|
||||
test('it checks FRONTEND_CSS_URL config', 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,
|
||||
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
@@ -173,14 +119,32 @@ test.describe('Config', () => {
|
||||
.first(),
|
||||
).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Config: Not loggued', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it checks that theme is configured from config endpoint', async ({
|
||||
test('it checks theme_customization.translations config', async ({
|
||||
page,
|
||||
}) => {
|
||||
await overrideConfig(page, {
|
||||
theme_customization: {
|
||||
translations: {
|
||||
en: {
|
||||
translation: {
|
||||
Docs: 'MyCustomDocs',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByText('MyCustomDocs')).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Config: Not logged', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it checks the config api is called', async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/config/') && response.status() === 200,
|
||||
@@ -191,11 +155,25 @@ test.describe('Config: Not loggued', () => {
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const jsonResponse = await response.json();
|
||||
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('default');
|
||||
const json = (await response.json()) as typeof CONFIG;
|
||||
const { theme_customization, ...configApi } = json;
|
||||
expect(theme_customization).toBeDefined();
|
||||
const { theme_customization: _, ...CONFIG_LEFT } = CONFIG;
|
||||
|
||||
const footer = page.locator('footer').first();
|
||||
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();
|
||||
// alt 'Gouvernement Logo' comes from the theme
|
||||
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('Doc Create', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Create: Not loggued', () => {
|
||||
test.describe('Doc Create: Not logged', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('it creates a doc server way', async ({
|
||||
@@ -44,8 +44,8 @@ test.describe('Doc Create: Not loggued', () => {
|
||||
const data = {
|
||||
title,
|
||||
content: markdown,
|
||||
sub: `user@${browserName}.e2e`,
|
||||
email: `user@${browserName}.e2e`,
|
||||
sub: `user@${browserName}.test`,
|
||||
email: `user@${browserName}.test`,
|
||||
};
|
||||
|
||||
const newDoc = await request.post(
|
||||
|
||||
@@ -4,6 +4,8 @@ import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
|
||||
import {
|
||||
CONFIG,
|
||||
addNewMember,
|
||||
createDoc,
|
||||
goToGridDoc,
|
||||
mockedDocument,
|
||||
@@ -93,7 +95,7 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
||||
|
||||
// When the visibility is changed, the ws should closed the connection (backend signal)
|
||||
// When the visibility is changed, the ws should close the connection (backend signal)
|
||||
const wsClosePromise = webSocket.waitForEvent('close');
|
||||
|
||||
await selectVisibility.click();
|
||||
@@ -363,7 +365,7 @@ test.describe('Doc Editor', () => {
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
link_reach: 'public',
|
||||
link_reach: 'restricted',
|
||||
link_role: 'editor',
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
title: '',
|
||||
@@ -423,6 +425,10 @@ 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);
|
||||
|
||||
@@ -437,6 +443,8 @@ 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();
|
||||
|
||||
@@ -452,4 +460,136 @@ 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 beeing displayed correctly
|
||||
* but it does not tell us if the images are being displayed correctly
|
||||
* in the pdf.
|
||||
*
|
||||
* TODO: Check if the images are displayed correctly in the pdf
|
||||
@@ -270,7 +270,7 @@ test.describe('Doc Export', () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* We cannot assert the line break is visible in the pdf but we can assert the
|
||||
* We cannot assert the line break is visible in the pdf, but we can assert the
|
||||
* line break is visible in the editor and that the pdf is generated.
|
||||
*/
|
||||
test('it exports the doc with divider', async ({ page, browserName }) => {
|
||||
|
||||
@@ -208,7 +208,6 @@ 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();
|
||||
|
||||
@@ -131,7 +131,7 @@ test.describe('Document list members', () => {
|
||||
const list = page.getByTestId('doc-share-quick-search');
|
||||
await expect(list).toBeVisible();
|
||||
const currentUser = list.getByTestId(
|
||||
`doc-share-member-row-user@${browserName}.e2e`,
|
||||
`doc-share-member-row-user@${browserName}.test`,
|
||||
);
|
||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
||||
await expect(currentUser).toBeVisible();
|
||||
@@ -175,7 +175,7 @@ test.describe('Document list members', () => {
|
||||
|
||||
const list = page.getByTestId('doc-share-quick-search');
|
||||
|
||||
const emailMyself = `user@${browserName}.e2e`;
|
||||
const emailMyself = `user@${browserName}.test`;
|
||||
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
|
||||
const mySelfMoreActions = mySelf.getByRole('button', {
|
||||
name: 'more_horiz',
|
||||
|
||||
@@ -96,7 +96,7 @@ test.describe('Doc Routing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Routing: Not loggued', () => {
|
||||
test.describe('Doc Routing: Not logged', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('checks redirect to a doc after login', async ({
|
||||
|
||||
@@ -101,6 +101,8 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
|
||||
@@ -121,14 +123,16 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(
|
||||
page.getByText('You do not have permission to view this document.'),
|
||||
).toBeVisible();
|
||||
).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test('A doc is accessible when member.', async ({ page, browserName }) => {
|
||||
@@ -147,7 +151,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
});
|
||||
|
||||
const otherBrowser = browsersName.find((b) => b !== browserName);
|
||||
const username = `user@${otherBrowser}.e2e`;
|
||||
const username = `user@${otherBrowser}.test`;
|
||||
await inputSearch.fill(username);
|
||||
await page.getByRole('option', { name: username }).click();
|
||||
|
||||
@@ -173,7 +177,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
@@ -291,7 +295,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Visibility mode').click();
|
||||
await page.getByLabel('Edition').click();
|
||||
await page.getByLabel('Editing').click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.').first(),
|
||||
@@ -329,7 +333,7 @@ test.describe('Doc Visibility: Public', () => {
|
||||
test.describe('Doc Visibility: Authenticated', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('A doc is not accessible when unauthentified.', async ({
|
||||
test('A doc is not accessible when unauthenticated.', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
@@ -430,7 +434,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
@@ -472,7 +476,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
|
||||
const urlDoc = page.url();
|
||||
await page.getByLabel('Visibility mode').click();
|
||||
await page.getByLabel('Edition').click();
|
||||
await page.getByLabel('Editing').click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.').first(),
|
||||
@@ -490,7 +494,7 @@ test.describe('Doc Visibility: Authenticated', () => {
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -1,14 +1,115 @@
|
||||
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();
|
||||
@@ -18,53 +119,30 @@ test.describe('Footer', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'service-public.fr' }),
|
||||
footer.getByRole('link', { name: 'Legal link' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'data.gouv.fr' }),
|
||||
footer.getByText('Some bottom information text'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'Legal Notice' }),
|
||||
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"),
|
||||
).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',
|
||||
),
|
||||
footer.getByRole('link', { name: 'un label personnalisé' }),
|
||||
).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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { expectLoginPage, keyCloakSignIn } from './common';
|
||||
import { expectLoginPage, keyCloakSignIn, overrideConfig } from './common';
|
||||
|
||||
test.describe('Header', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByLabel('Docs Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
|
||||
'color',
|
||||
'rgb(0, 0, 145)',
|
||||
'font-family',
|
||||
/Roboto/i,
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -36,6 +55,11 @@ 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(
|
||||
@@ -68,11 +92,13 @@ test.describe('Header', () => {
|
||||
test.describe('Header mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_THEME: 'dsfr',
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { CONFIG } from './common';
|
||||
import { overrideConfig } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/docs/');
|
||||
@@ -9,6 +9,62 @@ 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();
|
||||
@@ -54,18 +110,8 @@ test.describe('Home page', () => {
|
||||
});
|
||||
|
||||
test('it checks the homepage feature flag', 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,
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
await overrideConfig(page, {
|
||||
FRONTEND_HOMEPAGE_FEATURE_ENABLED: false,
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
@@ -128,8 +128,16 @@ export async function waitForLanguageSwitch(
|
||||
lang: TestLanguageValue,
|
||||
) {
|
||||
const header = page.locator('header').first();
|
||||
await header.getByRole('button', { name: 'arrow_drop_down' }).click();
|
||||
const languagePicker = header.locator('.--docs--language-picker-text');
|
||||
const isAlreadyTargetLanguage = await languagePicker
|
||||
.innerText()
|
||||
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
|
||||
|
||||
if (isAlreadyTargetLanguage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await languagePicker.click();
|
||||
const responsePromise = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/user') && resp.request().method() === 'PATCH',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user