Compare commits

..

4 Commits

Author SHA1 Message Date
Nathan Panchout 225366e92d (e2e) update test suite to use '/docs/' route
- Modify all test files to navigate to '/docs/' instead of '/'
- Add explicit login button click for authentication tests
- Update routing and navigation expectations in test specs
- Introduce new home page test spec
- Ensure consistent authentication flow across test scenarios
2025-01-29 17:21:02 +01:00
Nathan Panchout 2d0a50984b (frontend) implement home page and authentication flow
- Add home page components with responsive design
- Update authentication logic to handle direct login and redirects
- Modify header, layout, and routing to support new home page
- Introduce ProConnectButton for simplified login experience
- Refactor auth store and login mechanisms
2025-01-29 17:19:21 +01:00
Nathan Panchout 7bb6924292 (frontend) add new color and font size tokens for Cunningham
- Add 'primary-action' color token
- Introduce alternative font size tokens (xl-alt, lg-alt, md-alt,
sm-alt, xs-alt)
- Update button border colors to use greyscale-300
2025-01-29 17:19:21 +01:00
Nathan Panchout 3f420fcffc (frontend) add home page assets and update changelog
Add various assets for the home page feature, including logos,
screenshots, and banner images. Update CHANGELOG.md to reflect the new
home page feature.
2025-01-29 17:19:21 +01:00
203 changed files with 4166 additions and 6645 deletions
+2 -3
View File
@@ -7,11 +7,10 @@ on:
- 'release/**'
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
synchronize-with-crowdin:
runs-on: ubuntu-latest
+3 -12
View File
@@ -7,15 +7,13 @@ on:
- main
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
with-build_mails: true
synchronize-with-crowdin:
needs: install-dependencies
needs: install-front
runs-on: ubuntu-latest
steps:
@@ -31,13 +29,6 @@ jobs:
- name: Install development dependencies
run: pip install --user .
working-directory: src/backend
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
fail-on-cache-miss: true
- name: Install gettext
run: |
sudo apt-get update
-85
View File
@@ -1,85 +0,0 @@
name: Dependency reusable workflow
on:
workflow_call:
inputs:
node_version:
required: false
default: '20.x'
type: string
with-front-dependencies-installation:
type: boolean
default: false
with-build_mails:
type: boolean
default: false
jobs:
front-dependencies-installation:
if: ${{ inputs.with-front-dependencies-installation == true }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore the frontend cache
uses: actions/cache@v4
id: front-node_modules
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
- name: Setup Node.js
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Install dependencies
if: steps.front-node_modules.outputs.cache-hit != 'true'
run: cd src/frontend/ && yarn install --frozen-lockfile
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
build-mails:
if: ${{ inputs.with-build_mails == true }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Setup Node.js
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g yarn
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
@@ -0,0 +1,36 @@
name: Install frontend installation reusable workflow
on:
workflow_call:
inputs:
node_version:
required: false
default: '20.x'
type: string
jobs:
front-dependencies-installation:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore the frontend cache
uses: actions/cache@v4
id: front-node_modules
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
- name: Setup Node.js
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Install dependencies
if: steps.front-node_modules.outputs.cache-hit != 'true'
run: cd src/frontend/ && yarn install --frozen-lockfile
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
+27 -6
View File
@@ -10,14 +10,13 @@ on:
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
test-front:
needs: install-dependencies
needs: install-front
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -40,7 +39,7 @@ jobs:
lint-front:
runs-on: ubuntu-latest
needs: install-dependencies
needs: install-front
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -61,7 +60,7 @@ jobs:
test-e2e-chromium:
runs-on: ubuntu-latest
needs: install-dependencies
needs: install-front
timeout-minutes: 20
steps:
- name: Checkout repository
@@ -88,6 +87,28 @@ jobs:
- name: Start Docker services
run: make bootstrap FLUSH_ARGS='--no-input' cache=
# Tool to wait for a service to be ready
- name: Install Dockerize
run: |
curl -sSL https://github.com/jwilder/dockerize/releases/download/v0.8.0/dockerize-linux-amd64-v0.8.0.tar.gz | sudo tar -C /usr/local/bin -xzv
- name: Wait for services to be ready
run: |
printf "Minio check...\n"
dockerize -wait tcp://localhost:9000 -timeout 20s
printf "Keyclock check...\n"
dockerize -wait tcp://localhost:8080 -timeout 20s
printf "Server collaboration check...\n"
dockerize -wait tcp://localhost:4444 -timeout 20s
printf "Ngnix check...\n"
dockerize -wait tcp://localhost:8083 -timeout 20s
printf "DRF check...\n"
dockerize -wait tcp://localhost:8071 -timeout 20s
printf "Postgres Keyclock check...\n"
dockerize -wait tcp://localhost:5433 -timeout 20s
printf "Postgres back check...\n"
dockerize -wait tcp://localhost:15432 -timeout 20s
- name: Run e2e tests
run: cd src/frontend/ && yarn e2e:test --project='chromium'
+41 -7
View File
@@ -9,11 +9,6 @@ on:
- "*"
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
with-build_mails: true
lint-git:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests
@@ -61,6 +56,46 @@ jobs:
exit 1
fi
build-mails:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g yarn
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
lint-back:
runs-on: ubuntu-latest
defaults:
@@ -86,7 +121,7 @@ jobs:
test-back:
runs-on: ubuntu-latest
needs: install-dependencies
needs: build-mails
defaults:
run:
@@ -134,7 +169,6 @@ jobs:
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
fail-on-cache-miss: true
- name: Start MinIO
run: |
+24 -44
View File
@@ -6,40 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## Changed
- 🛂(frontend) Restore version visibility #629
- 📝(doc) minor README.md formatting and wording enhancements
- ♻️Stop setting a default title on doc creation #634
- 📝(readme) remove front-end local run instructions local.md #651
## Fixed
- ♻️(frontend) improve table pdf rendering
## [2.2.0] - 2025-02-10
## Added
- 📝(doc) Add security.md and codeofconduct.md #604
- ✨(frontend) add home page #608
- ✨(frontend) cursor display on activity #609
- ✨(frontend) Add export page break #623
## Changed
- 🔧(backend) make AI feature reach configurable #628
## Fixed
- 🌐(CI) Fix email partially translated #616
- 🐛(frontend) fix cursor breakline #609
- 🐛(frontend) fix style pdf export #609
## [2.1.0] - 2025-01-29
## Added
- ✨(backend) add soft delete and restore API endpoints to documents #516
@@ -53,13 +22,11 @@ and this project adheres to
## Changed
- 💄(frontend) add abilities on doc row #581
- 💄(frontend) improve DocsGridItem responsive padding #582
- 🔧(backend) Bump maximum page size to 200 #516
- 📝(doc) Improve Read me #558
- 💄(frontend) improve DocsGridItem responsive padding #582
## Fixed
## Changed
- 🐛Fix invitations #575
- 🔧(backend) Bump page size to 200 #516
## Removed
@@ -84,6 +51,7 @@ and this project adheres to
- ✨(frontend) add favorite feature #515
- 📝(documentation) Documentation about self-hosted installation #530
- ✨(helm) helm versioning #530
- ✨(frontend) add home page #553
## Changed
@@ -119,11 +87,12 @@ and this project adheres to
- ⚡️(e2e) reduce flakiness on e2e tests #511
## Fixed
## Fixed
- 🐛(frontend) update doc editor height #481
- 💄(frontend) add doc search #485
## [1.9.0] - 2024-12-11
## Added
@@ -145,18 +114,21 @@ and this project adheres to
- 🐛(frontend) Fix hidden menu on Firefox #468
- 🐛(backend) fix sanitize problem IA #490
## [1.8.2] - 2024-11-28
## Changed
- ♻️(SW) change strategy html caching #460
## [1.8.1] - 2024-11-27
## Fixed
- 🐛(frontend) link not clickable and flickering firefox #457
## [1.8.0] - 2024-11-25
## Added
@@ -185,6 +157,7 @@ and this project adheres to
- 🐛(frontend) users have view access when revoked #387
- 🐛(frontend) fix placeholder editable when double clicks #454
## [1.7.0] - 2024-10-24
## Added
@@ -212,6 +185,7 @@ and this project adheres to
- 🔥(helm) remove infra related codes #366
## [1.6.0] - 2024-10-17
## Added
@@ -234,6 +208,7 @@ and this project adheres to
- 🐛(backend) fix nginx docker container #340
- 🐛(frontend) fix copy paste firefox #353
## [1.5.1] - 2024-10-10
## Fixed
@@ -268,6 +243,7 @@ and this project adheres to
- 🔧(backend) fix configuration to avoid different ssl warning #297
- 🐛(frontend) fix editor break line not working #302
## [1.4.0] - 2024-09-17
## Added
@@ -288,6 +264,7 @@ and this project adheres to
- 🐛(backend) Fix forcing ID when creating a document via API endpoint #234
- 🐛 Rebuild frontend dev container from makefile #248
## [1.3.0] - 2024-09-05
## Added
@@ -312,6 +289,7 @@ and this project adheres to
- 🔥(frontend) remove saving modal #213
## [1.2.1] - 2024-08-23
## Changed
@@ -319,6 +297,7 @@ and this project adheres to
- ♻️ Change ordering docs datagrid #195
- 🔥(helm) use scaleway email #194
## [1.2.0] - 2024-08-22
## Added
@@ -342,14 +321,14 @@ and this project adheres to
- ⚡️(CI) only e2e chrome mandatory #177
## Removed
- 🔥(helm) remove htaccess #181
## [1.1.0] - 2024-07-15
## Added
- 🤡(demo) generate dummy documents on dev users #120
- 🤡(demo) generate dummy documents on dev users #120
- ✨(frontend) create side modal component #134
- ✨(frontend) Doc grid actions (update / delete) #136
- ✨(frontend) Doc editor header information #137
@@ -360,11 +339,12 @@ and this project adheres to
- ♻️(frontend) create a doc from a modal #132
- ♻️(frontend) manage members from the share modal #140
## [1.0.0] - 2024-07-02
## Added
- 🛂(frontend) Manage the document's right (#75)
- 🛂(frontend) Manage the document's right (#75)
- ✨(frontend) Update document (#68)
- ✨(frontend) Remove document (#68)
- 🐳(docker) dockerize dev frontend (#63)
@@ -398,6 +378,7 @@ and this project adheres to
- 💚(CI) Remove trigger workflow on push tags on CI (#68)
- 🔥(frontend) Remove coming soon page (#121)
## [0.1.0] - 2024-05-24
## Added
@@ -405,9 +386,8 @@ 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/v2.2.0...main
[v2.2.0]: https://github.com/numerique-gouv/impress/releases/v2.2.0
[v2.1.0]: https://github.com/numerique-gouv/impress/releases/v2.1.0
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.0.1...main
[v2.0.1]: https://github.com/numerique-gouv/impress/releases/v2.0.1
[v2.0.0]: https://github.com/numerique-gouv/impress/releases/v2.0.0
[v1.10.0]: https://github.com/numerique-gouv/impress/releases/v1.10.0
-75
View File
@@ -1,75 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
- Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at docs@numerique.gouv.fr.
- All complaints will be reviewed and investigated promptly and fairly.
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this
## Code of Conduct:
1. Correction
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
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
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
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
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.
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.
+17 -13
View File
@@ -44,6 +44,7 @@ COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev
COMPOSE_RUN = $(COMPOSE) run --rm
COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev
COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s
# -- Backend
MANAGE = $(COMPOSE_RUN_APP) python manage.py
@@ -80,12 +81,12 @@ bootstrap: \
data/static \
create-env-files \
build \
run-with-frontend \
migrate \
demo \
back-i18n-compile \
mails-install \
mails-build \
run
mails-build
.PHONY: bootstrap
# -- Docker/compose
@@ -108,7 +109,7 @@ build-yjs-provider: ## build the y-provider container
build-frontend: cache ?=
build-frontend: ## build the frontend container
@$(COMPOSE) build frontend $(cache)
@$(COMPOSE) build frontend-dev $(cache)
.PHONY: build-frontend
down: ## stop and remove containers, networks, images, and volumes
@@ -119,18 +120,19 @@ logs: ## display app-dev logs (follow mode)
@$(COMPOSE) logs -f app-dev
.PHONY: logs
run-backend: ## Start only the backend application and all needed services
run: ## start the wsgi (production) and development server
@$(COMPOSE) up --force-recreate -d celery-dev
@$(COMPOSE) up --force-recreate -d y-provider
@$(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
@echo "Wait for postgresql to be up..."
@$(WAIT_DB)
.PHONY: run
run-with-frontend: ## Start all the containers needed (backend to frontend)
@$(MAKE) run
@$(COMPOSE) up --force-recreate -d frontend-dev
.PHONY: run-with-frontend
status: ## an alias for "docker compose ps"
@$(COMPOSE) ps
.PHONY: status
@@ -186,12 +188,14 @@ test-back-parallel: ## run all back-end tests in parallel
makemigrations: ## run django makemigrations for the impress project.
@echo "$(BOLD)Running makemigrations$(RESET)"
@$(COMPOSE) up -d postgresql
@$(WAIT_DB)
@$(MANAGE) makemigrations
.PHONY: makemigrations
migrate: ## run django migrations for the impress project.
@echo "$(BOLD)Running migrations$(RESET)"
@$(COMPOSE) up -d postgresql
@$(WAIT_DB)
@$(MANAGE) migrate
.PHONY: migrate
@@ -306,16 +310,16 @@ help:
.PHONY: help
# Front
frontend-development-install: ## install the frontend locally
frontend-install: ## install the frontend locally
cd $(PATH_FRONT_IMPRESS) && yarn
.PHONY: frontend-development-install
.PHONY: frontend-install
frontend-lint: ## run the frontend linter
cd $(PATH_FRONT) && yarn lint
.PHONY: frontend-lint
run-frontend-development: ## Run the frontend in development mode
@$(COMPOSE) stop frontend
@$(COMPOSE) stop frontend-dev
cd $(PATH_FRONT_IMPRESS) && yarn dev
.PHONY: run-frontend-development
+36 -33
View File
@@ -13,17 +13,14 @@ Welcome to Docs! The open source document editor where your notes can become kno
Chat on Matrix
</a> - <a href="/docs/">
Documentation
</a> - <a href="#getting-started-">
</a> - <a href="#getting-started">
Getting started
</a> - <a href="mailto:docs@numerique.gouv.fr">
Reach out
</a>
</p>
<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
@@ -34,31 +31,23 @@ Docs is a collaborative text editor designed to address common challenges in kno
* ✨ Save time thanks to our AI actions (generate, sum up, correct, translate)
### Collaborate
* 🤝 Collaborate with your team in real time
* 🔒 Granular access control to ensure your information is secure and only shared with the right people
* 🤝 Collaborate in realtime with your team mates
* 🔒 Granular access control to keep your information secure and shared with the right people
* 📑 Professional document exports in multiple formats (.odt, .doc, .pdf) with customizable templates
* 📚 Built-in wiki functionality to turn your team's collaborative work into organized knowledge `ETA 02/2025`
* 📚 Built-in wiki functionality to transform your team's collaborative work into organized knowledge `ETA 02/2025`
### Self-host
* 🚀 Easy to install, scalable and secure alternative to Notion and Outline.
* 🚀 Easy to install, scalable and secure alternative to Notion, Outline or Confluence
## Getting started 🔧
### Test it
Test Docs on your browser by logging in on this [environment](https://impress-preprod.beta.numerique.gouv.fr/docs/0aa856e9-da41-4d59-b73d-a61cb2c1245f/)
```
email: test.docs@yopmail.com
password: I'd<3ToTestDocs
```
### Run it locally
> ⚠️ Running Docs locally using the methods described below is for testing purposes only. It is based on building Docs using Minio as the S3 storage solution: if you want to use Minio for production deployment of Docs, you will need to comply with Minio's AGPL-3.0 licence.
**Prerequisite**
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop:
```shellscript
@@ -66,22 +55,23 @@ $ docker -v
Docker version 20.10.2, build 2291f61
$ docker compose version
$ docker compose -v
Docker Compose version v2.32.4
docker compose version 1.27.4, build 40524192
```
> ⚠️ 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 assigning your user to the `docker` group.
**Project bootstrap**
The easiest way to start working on the project is to use GNU Make:
```shellscript
$ 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` 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-releated or migration-releated issues.
Your Docker services should now be up and running 🎉
@@ -89,13 +79,34 @@ You can access to the project by going to <http://localhost:3000>.
You will be prompted to log in, the default credentials are:
```
```shellscript
username: impress
password: impress
```
📝 Note that if you need to run them afterwards, you can use the eponym Make rule:
```shellscript
$ make run-with-frontend
```
⚠️ For the frontend developer, it is often better to run the frontend in development mode locally.
To do so, install the frontend dependencies with the following command:
```shellscript
$ make frontend-install
```
And run the frontend locally in development mode with the following command:
```shellscript
$ make run-frontend-development
```
To start all the services, except the frontend container, you can use the following command:
```shellscript
$ make run
```
@@ -114,7 +125,6 @@ $ make help
```
**Django admin**
You can access the Django admin site at
<http://localhost:8071/admin>.
@@ -126,21 +136,17 @@ $ 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).
## Roadmap
Want to know where the project is headed? [🗺️ Checkout our roadmap](https://github.com/orgs/numerique-gouv/projects/13/views/11)
## Licence 📝
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.
## Contributing 🙌
This project is intended to be community-driven, so please, do not hesitate to [get in touch](https://matrix.to/#/#docs-official:matrix.org) if you have any question related to our implementation or design decisions.
You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
@@ -162,18 +168,15 @@ docs
```
## Credits ❤️
### 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/), [MinIO](https://min.io/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/)
### Gov ❤️ open source
Docs is the result of a joint effort led by the French 🇫🇷🥖 ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German 🇩🇪🥨 governments ([ZenDiS](https://zendis.de/)).
Docs is the result of a joint effort lead 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.
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱🧀), feel free to [reach out](https://matrix.to/#/#docs-official:matrix.org) if you are interested in using or contributing to Docs.
<p align="center">
<img src="/docs/assets/europe_opensource.png" width="50%"/>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

-23
View File
@@ -1,23 +0,0 @@
# Security Policy
## Reporting a Vulnerability
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
We appreciate your effort to make Docs more secure.
## Vulnerability disclosure policy
Working with security issues in an open source project can be challenging, as we are required to disclose potential problems that could be exploited by attackers. With this in mind, our security fix policy is as follows:
1. The Maintainers team will handle the fix as usual (Pull Request,
release).
2. In the release notes, we will include the identification numbers from the
GitHub Advisory Database (GHSA) and, if applicable, the Common Vulnerabilities
and Exposures (CVE) identifier for the vulnerability.
3. Once this grace period has passed, we will publish the vulnerability.
By adhering to this security policy, we aim to address security concerns
effectively and responsibly in our open source software project.
-5
View File
@@ -15,8 +15,3 @@ the following command inside your docker container:
(Note : in your development environment, you can `make migrate`.)
## [Unreleased]
- AI features are now limited to users who are authenticated. Before this release, even anonymous
users who gained editor access on a document with link reach used to get AI feature.
IF you want anonymous users to keep access on AI features, you must now define the
`AI_ALLOW_REACH_FROM` setting to "public".
+3 -1
View File
@@ -7,6 +7,7 @@ UNSET_USER=0
TERRAFORM_DIRECTORY="./env.d/terraform"
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
COMPOSE_PROJECT="docs"
# _set_user: set (or unset) default user id used to run docker commands
@@ -39,8 +40,9 @@ function _set_user() {
# ARGS : docker compose command arguments
function _docker_compose() {
echo "🐳(compose) file: '${COMPOSE_FILE}'"
echo "🐳(compose) project: '${COMPOSE_PROJECT}' file: '${COMPOSE_FILE}'"
docker compose \
-p "${COMPOSE_PROJECT}" \
-f "${COMPOSE_FILE}" \
--project-directory "${REPO_DIR}" \
"$@"
+16 -54
View File
@@ -1,13 +1,6 @@
name: docs
services:
postgresql:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 1s
timeout: 2s
retries: 300
env_file:
- env.d/development/postgresql
ports:
@@ -30,11 +23,6 @@ services:
ports:
- '9000:9000'
- '9001:9001'
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 20s
retries: 300
entrypoint: ""
command: minio server --console-address :9001 /data
volumes:
@@ -43,9 +31,7 @@ services:
createbuckets:
image: minio/mc
depends_on:
minio:
condition: service_healthy
restart: true
- minio
entrypoint: >
sh -c "
/usr/bin/mc alias set impress http://minio:9000 impress password && \
@@ -73,15 +59,10 @@ services:
- ./src/backend:/app
- ./data/static:/data/static
depends_on:
postgresql:
condition: service_healthy
restart: true
mailcatcher:
condition: service_started
redis:
condition: service_started
createbuckets:
condition: service_started
- postgresql
- mailcatcher
- redis
- createbuckets
celery-dev:
user: ${DOCKER_USER:-1000}
@@ -112,13 +93,9 @@ services:
- env.d/development/common
- env.d/development/postgresql
depends_on:
postgresql:
condition: service_healthy
restart: true
redis:
condition: service_started
minio:
condition: service_started
- postgresql
- redis
- minio
celery:
user: ${DOCKER_USER:-1000}
@@ -139,15 +116,11 @@ services:
volumes:
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
app-dev:
condition: service_started
y-provider:
condition: service_started
keycloak:
condition: service_healthy
restart: true
- keycloak
- app-dev
- y-provider
frontend:
frontend-dev:
user: "${DOCKER_USER:-1000}"
build:
context: .
@@ -162,6 +135,9 @@ services:
ports:
- "3000:3000"
dockerize:
image: jwilder/dockerize
crowdin:
image: crowdin/cli:3.16.0
volumes:
@@ -193,11 +169,6 @@ services:
kc_postgresql:
image: postgres:14.3
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 1s
timeout: 2s
retries: 300
ports:
- "5433:5432"
env_file:
@@ -216,13 +187,6 @@ services:
- --hostname-admin-url=http://localhost:8083/
- --hostname-strict=false
- --hostname-strict-https=false
- --health-enabled=true
- --metrics-enabled=true
healthcheck:
test: ["CMD", "curl", "--head", "-fsS", "http://localhost:8080/health/ready"]
interval: 1s
timeout: 2s
retries: 300
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
@@ -236,6 +200,4 @@ services:
ports:
- "8080:8080"
depends_on:
kc_postgresql:
condition: service_healthy
restart: true
- kc_postgresql
@@ -88,11 +88,5 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Increase proxy buffer size to allow keycloak to send large
# header responses when a user is created.
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
-92
View File
@@ -1,92 +0,0 @@
# 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: if you want to use Minio for production deployment of Docs, you will need to comply with Minio's AGPL-3.0 licence.
**Prerequisite**
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop:
```shellscript
$ docker -v
Docker version 20.10.2, build 2291f61
$ 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.
**Project bootstrap**
The easiest way to start working on the project is to use GNU Make:
```shellscript
$ 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.
Your Docker services should now be up and running 🎉
You can access to the project by going to <http://localhost:3000>.
You will be prompted to log in, the default credentials are:
```
username: impress
password: impress
```
📝 Note that if you need to run them afterwards, you can use the eponym Make rule:
```shellscript
$ make run
```
**Adding content**
You can create a basic demo site by running:
```shellscript
$ make demo
```
Finally, you can check all available Make rules using:
```shellscript
$ make help
```
**Django admin**
You can access the Django admin site at
<http://localhost:8071/admin>.
You first need to create a superuser account:
```shellscript
$ make superuser
```
## Front-end dev instructions
⚠️ For the frontend developer, it is often better to run the frontend in development mode locally.
To do so, install the frontend dependencies with the following command:
```shellscript
$ make frontend-development-install
```
And run the frontend locally in development mode with the following command:
```shellscript
$ make run-frontend-development
```
To start all the services, except the frontend container, you can use the following command:
```shellscript
$ make run-backend
```
+1 -6
View File
@@ -14,15 +14,10 @@
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@openfun/cunningham-react",
"@types/react",
"@types/react-dom",
"eslint",
"fetch-mock",
"node",
"node-fetch",
"react",
"react-dom",
"eslint",
"workbox-webpack-plugin"
]
}
+97 -483
View File
@@ -1,552 +1,166 @@
# Generated by Django 5.0.3 on 2024-05-28 20:29
import uuid
import django.contrib.auth.models
import django.core.validators
import django.db.models.deletion
import timezone_field.fields
import uuid
from django.conf import settings
from django.db import migrations, models
import timezone_field.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name="Document",
name='Document',
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("title", models.CharField(max_length=255, verbose_name="title")),
(
"is_public",
models.BooleanField(
default=False,
help_text="Whether this document is public for anyone to use.",
verbose_name="public",
),
),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('title', models.CharField(max_length=255, verbose_name='title')),
('is_public', models.BooleanField(default=False, help_text='Whether this document is public for anyone to use.', verbose_name='public')),
],
options={
"verbose_name": "Document",
"verbose_name_plural": "Documents",
"db_table": "impress_document",
"ordering": ("title",),
'verbose_name': 'Document',
'verbose_name_plural': 'Documents',
'db_table': 'impress_document',
'ordering': ('title',),
},
),
migrations.CreateModel(
name="Template",
name='Template',
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("title", models.CharField(max_length=255, verbose_name="title")),
(
"description",
models.TextField(blank=True, verbose_name="description"),
),
("code", models.TextField(blank=True, verbose_name="code")),
("css", models.TextField(blank=True, verbose_name="css")),
(
"is_public",
models.BooleanField(
default=False,
help_text="Whether this template is public for anyone to use.",
verbose_name="public",
),
),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('title', models.CharField(max_length=255, verbose_name='title')),
('description', models.TextField(blank=True, verbose_name='description')),
('code', models.TextField(blank=True, verbose_name='code')),
('css', models.TextField(blank=True, verbose_name='css')),
('is_public', models.BooleanField(default=False, help_text='Whether this template is public for anyone to use.', verbose_name='public')),
],
options={
"verbose_name": "Template",
"verbose_name_plural": "Templates",
"db_table": "impress_template",
"ordering": ("title",),
'verbose_name': 'Template',
'verbose_name_plural': 'Templates',
'db_table': 'impress_template',
'ordering': ('title',),
},
),
migrations.CreateModel(
name="User",
name='User',
fields=[
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"sub",
models.CharField(
blank=True,
help_text="Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.",
max_length=255,
null=True,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.",
regex="^[\\w.@+-]+\\Z",
)
],
verbose_name="sub",
),
),
(
"email",
models.EmailField(
blank=True,
max_length=254,
null=True,
verbose_name="identity email address",
),
),
(
"admin_email",
models.EmailField(
blank=True,
max_length=254,
null=True,
unique=True,
verbose_name="admin email address",
),
),
(
"language",
models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
(
"timezone",
timezone_field.fields.TimeZoneField(
choices_display="WITH_GMT_OFFSET",
default="UTC",
help_text="The timezone in which the user wants to see times.",
use_pytz=False,
),
),
(
"is_device",
models.BooleanField(
default=False,
help_text="Whether the user is a device or a real user.",
verbose_name="device",
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('sub', models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.', regex='^[\\w.@+-]+\\Z')], verbose_name='sub')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')),
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')),
('language', models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
('timezone', timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='UTC', help_text='The timezone in which the user wants to see times.', use_pytz=False)),
('is_device', models.BooleanField(default=False, help_text='Whether the user is a device or a real user.', verbose_name='device')),
('is_staff', models.BooleanField(default=False, help_text='Whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"db_table": "impress_user",
'verbose_name': 'user',
'verbose_name_plural': 'users',
'db_table': 'impress_user',
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name="DocumentAccess",
name='DocumentAccess',
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("team", models.CharField(blank=True, max_length=100)),
(
"role",
models.CharField(
choices=[
("reader", "Reader"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="accesses",
to="core.document",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('team', models.CharField(blank=True, max_length=100)),
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.document')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
"verbose_name": "Document/user relation",
"verbose_name_plural": "Document/user relations",
"db_table": "impress_document_access",
"ordering": ("-created_at",),
'verbose_name': 'Document/user relation',
'verbose_name_plural': 'Document/user relations',
'db_table': 'impress_document_access',
'ordering': ('-created_at',),
},
),
migrations.CreateModel(
name="Invitation",
name='Invitation',
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"email",
models.EmailField(max_length=254, verbose_name="email address"),
),
(
"role",
models.CharField(
choices=[
("reader", "Reader"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="invitations",
to="core.document",
),
),
(
"issuer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="invitations",
to=settings.AUTH_USER_MODEL,
),
),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('email', models.EmailField(max_length=254, verbose_name='email address')),
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.document')),
('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)),
],
options={
"verbose_name": "Document invitation",
"verbose_name_plural": "Document invitations",
"db_table": "impress_invitation",
'verbose_name': 'Document invitation',
'verbose_name_plural': 'Document invitations',
'db_table': 'impress_invitation',
},
),
migrations.CreateModel(
name="TemplateAccess",
name='TemplateAccess',
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("team", models.CharField(blank=True, max_length=100)),
(
"role",
models.CharField(
choices=[
("reader", "Reader"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
(
"template",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="accesses",
to="core.template",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('team', models.CharField(blank=True, max_length=100)),
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.template')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
"verbose_name": "Template/user relation",
"verbose_name_plural": "Template/user relations",
"db_table": "impress_template_access",
"ordering": ("-created_at",),
'verbose_name': 'Template/user relation',
'verbose_name_plural': 'Template/user relations',
'db_table': 'impress_template_access',
'ordering': ('-created_at',),
},
),
migrations.AddConstraint(
model_name="documentaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("user__isnull", False)),
fields=("user", "document"),
name="unique_document_user",
violation_error_message="This user is already in this document.",
),
model_name='documentaccess',
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'document'), name='unique_document_user', violation_error_message='This user is already in this document.'),
),
migrations.AddConstraint(
model_name="documentaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("team__gt", "")),
fields=("team", "document"),
name="unique_document_team",
violation_error_message="This team is already in this document.",
),
model_name='documentaccess',
constraint=models.UniqueConstraint(condition=models.Q(('team__gt', '')), fields=('team', 'document'), name='unique_document_team', violation_error_message='This team is already in this document.'),
),
migrations.AddConstraint(
model_name="documentaccess",
constraint=models.CheckConstraint(
check=models.Q(
models.Q(("team", ""), ("user__isnull", False)),
models.Q(("team__gt", ""), ("user__isnull", True)),
_connector="OR",
),
name="check_document_access_either_user_or_team",
violation_error_message="Either user or team must be set, not both.",
),
model_name='documentaccess',
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
),
migrations.AddConstraint(
model_name="invitation",
constraint=models.UniqueConstraint(
fields=("email", "document"), name="email_and_document_unique_together"
),
model_name='invitation',
constraint=models.UniqueConstraint(fields=('email', 'document'), name='email_and_document_unique_together'),
),
migrations.AddConstraint(
model_name="templateaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("user__isnull", False)),
fields=("user", "template"),
name="unique_template_user",
violation_error_message="This user is already in this template.",
),
model_name='templateaccess',
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'template'), name='unique_template_user', violation_error_message='This user is already in this template.'),
),
migrations.AddConstraint(
model_name="templateaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("team__gt", "")),
fields=("team", "template"),
name="unique_template_team",
violation_error_message="This team is already in this template.",
),
model_name='templateaccess',
constraint=models.UniqueConstraint(condition=models.Q(('team__gt', '')), fields=('team', 'template'), name='unique_template_team', violation_error_message='This team is already in this template.'),
),
migrations.AddConstraint(
model_name="templateaccess",
constraint=models.CheckConstraint(
check=models.Q(
models.Q(("team", ""), ("user__isnull", False)),
models.Q(("team__gt", ""), ("user__isnull", True)),
_connector="OR",
),
name="check_template_access_either_user_or_team",
violation_error_message="Either user or team must be set, not both.",
),
model_name='templateaccess',
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
),
]
@@ -1,9 +1,9 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0001_initial"),
('core', '0001_initial'),
]
operations = [
@@ -1,114 +1,52 @@
# Generated by Django 5.1 on 2024-09-08 16:55
import uuid
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0002_create_pg_trgm_extension"),
('core', '0002_create_pg_trgm_extension'),
]
operations = [
migrations.AddField(
model_name="document",
name="link_reach",
field=models.CharField(
choices=[
("restricted", "Restricted"),
("authenticated", "Authenticated"),
("public", "Public"),
],
default="authenticated",
max_length=20,
),
model_name='document',
name='link_reach',
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='authenticated', max_length=20),
),
migrations.AddField(
model_name="document",
name="link_role",
field=models.CharField(
choices=[("reader", "Reader"), ("editor", "Editor")],
default="reader",
max_length=20,
),
model_name='document',
name='link_role',
field=models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor')], default='reader', max_length=20),
),
migrations.AlterField(
model_name="document",
name="is_public",
model_name='document',
name='is_public',
field=models.BooleanField(null=True),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
),
migrations.CreateModel(
name="LinkTrace",
name='LinkTrace',
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="link_traces",
to="core.document",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="link_traces",
to=settings.AUTH_USER_MODEL,
),
),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to='core.document')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to=settings.AUTH_USER_MODEL)),
],
options={
"verbose_name": "Document/user link trace",
"verbose_name_plural": "Document/user link traces",
"db_table": "impress_link_trace",
"constraints": [
models.UniqueConstraint(
fields=("user", "document"),
name="unique_link_trace_document_user",
violation_error_message="A link trace already exists for this document/user.",
)
],
'verbose_name': 'Document/user link trace',
'verbose_name_plural': 'Document/user link traces',
'db_table': 'impress_link_trace',
'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_link_trace_document_user', violation_error_message='A link trace already exists for this document/user.')],
},
),
]
@@ -1,14 +1,13 @@
# Generated by Django 5.1 on 2024-09-08 17:04
from django.db import migrations
def migrate_is_public_to_link_reach(apps, schema_editor):
"""
Forward migration: Migrate 'is_public' to 'link_reach'.
If is_public == True, set link_reach to 'public'
"""
Document = apps.get_model("core", "Document")
Document.objects.filter(is_public=True).update(link_reach="public")
Document = apps.get_model('core', 'Document')
Document.objects.filter(is_public=True).update(link_reach='public')
def reverse_migrate_link_reach_to_is_public(apps, schema_editor):
@@ -17,20 +16,20 @@ def reverse_migrate_link_reach_to_is_public(apps, schema_editor):
- If link_reach == 'public', set is_public to True
- Else set is_public to False
"""
Document = apps.get_model("core", "Document")
Document.objects.filter(link_reach="public").update(is_public=True)
Document.objects.filter(link_reach__in=["restricted", "authenticated"]).update(
is_public=False
)
Document = apps.get_model('core', 'Document')
Document.objects.filter(link_reach='public').update(is_public=True)
Document.objects.filter(link_reach__in=['restricted', "authenticated"]).update(is_public=False)
class Migration(migrations.Migration):
dependencies = [
("core", "0003_document_link_reach_document_link_role_and_more"),
('core', '0003_document_link_reach_document_link_role_and_more'),
]
operations = [
migrations.RunPython(
migrate_is_public_to_link_reach, reverse_migrate_link_reach_to_is_public
migrate_is_public_to_link_reach,
reverse_migrate_link_reach_to_is_public
),
]
@@ -4,16 +4,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0004_migrate_is_public_to_link_reach"),
('core', '0004_migrate_is_public_to_link_reach'),
]
operations = [
migrations.AlterField(
model_name="document",
name="title",
field=models.CharField(
blank=True, max_length=255, null=True, verbose_name="title"
),
model_name='document',
name='title',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title'),
),
]
@@ -4,34 +4,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0005_remove_document_is_public_alter_document_link_reach_and_more"),
('core', '0005_remove_document_is_public_alter_document_link_reach_and_more'),
]
operations = [
migrations.AddField(
model_name="user",
name="full_name",
field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="full name"
),
model_name='user',
name='full_name',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='full name'),
),
migrations.AddField(
model_name="user",
name="short_name",
field=models.CharField(
blank=True, max_length=20, null=True, verbose_name="short name"
),
model_name='user',
name='short_name',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='short name'),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
),
]
@@ -117,10 +117,10 @@ BEGIN
END $$;
"""
class Migration(migrations.Migration):
dependencies = [
("core", "0006_add_user_full_name_and_short_name"),
('core', '0006_add_user_full_name_and_short_name'),
]
operations = [
@@ -4,22 +4,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0007_fix_users_duplicate"),
('core', '0007_fix_users_duplicate'),
]
operations = [
migrations.AlterField(
model_name="document",
name="link_reach",
field=models.CharField(
choices=[
("restricted", "Restricted"),
("authenticated", "Authenticated"),
("public", "Public"),
],
default="restricted",
max_length=20,
),
model_name='document',
name='link_reach',
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='restricted', max_length=20),
),
]
@@ -1,87 +1,37 @@
# Generated by Django 5.1.2 on 2024-11-08 07:59
import uuid
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0008_alter_document_link_reach"),
('core', '0008_alter_document_link_reach'),
]
operations = [
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
),
migrations.CreateModel(
name="DocumentFavorite",
name='DocumentFavorite',
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="favorited_by_users",
to="core.document",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="favorite_documents",
to=settings.AUTH_USER_MODEL,
),
),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by_users', to='core.document')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to=settings.AUTH_USER_MODEL)),
],
options={
"verbose_name": "Document favorite",
"verbose_name_plural": "Document favorites",
"db_table": "impress_document_favorite",
"constraints": [
models.UniqueConstraint(
fields=("user", "document"),
name="unique_document_favorite_user",
violation_error_message="This document is already targeted by a favorite relation instance for the same user.",
)
],
'verbose_name': 'Document favorite',
'verbose_name_plural': 'Document favorites',
'db_table': 'impress_document_favorite',
'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_document_favorite_user', violation_error_message='This document is already targeted by a favorite relation instance for the same user.')],
},
),
]
@@ -7,48 +7,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0009_add_document_favorite"),
('core', '0009_add_document_favorite'),
]
operations = [
migrations.AddField(
model_name="document",
name="creator",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="documents_created",
to=settings.AUTH_USER_MODEL,
),
model_name='document',
name='creator',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
),
migrations.AlterField(
model_name="user",
name="sub",
field=models.CharField(
blank=True,
help_text="Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.",
max_length=255,
null=True,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.",
regex="^[\\w.@+-:]+\\Z",
)
],
verbose_name="sub",
),
model_name='user',
name='sub',
field=models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.', regex='^[\\w.@+-:]+\\Z')], verbose_name='sub'),
),
]
@@ -3,7 +3,7 @@
import django.db.models.deletion
from django.conf import settings
from django.db import migrations
from django.db.models import F, ForeignKey, OuterRef, Q, Subquery
from django.db.models import F, ForeignKey, Subquery, OuterRef, Q
def set_creator_from_document_access(apps, schema_editor):
@@ -25,37 +25,28 @@ def set_creator_from_document_access(apps, schema_editor):
DocumentAccess = apps.get_model("core", "DocumentAccess")
# Update `creator` using the "owner" role
owner_subquery = (
DocumentAccess.objects.filter(
document=OuterRef("pk"),
user__isnull=False,
role="owner",
)
.order_by("created_at")
.values("user_id")[:1]
)
owner_subquery = DocumentAccess.objects.filter(
document=OuterRef('pk'),
user__isnull=False,
role='owner',
).order_by('created_at').values('user_id')[:1]
Document.objects.filter(creator__isnull=True).update(
creator=Subquery(owner_subquery)
)
Document.objects.filter(
creator__isnull=True
).update(creator=Subquery(owner_subquery))
class Migration(migrations.Migration):
dependencies = [
("core", "0010_add_field_creator_to_document"),
('core', '0010_add_field_creator_to_document'),
]
operations = [
migrations.RunPython(
set_creator_from_document_access, reverse_code=migrations.RunPython.noop
),
migrations.RunPython(set_creator_from_document_access, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name="document",
name="creator",
field=ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="documents_created",
to=settings.AUTH_USER_MODEL,
),
model_name='document',
name='creator',
field=ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL),
),
]
@@ -6,42 +6,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0011_populate_creator_field_and_make_it_required"),
('core', '0011_populate_creator_field_and_make_it_required'),
]
operations = [
migrations.AlterField(
model_name="document",
name="creator",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="documents_created",
to=settings.AUTH_USER_MODEL,
),
model_name='document',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name="invitation",
name="issuer",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="invitations",
to=settings.AUTH_USER_MODEL,
),
model_name='invitation',
name='issuer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
),
]
@@ -2,10 +2,10 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0012_make_document_creator_and_invitation_issuer_optional"),
('core', '0012_make_document_creator_and_invitation_issuer_optional'),
]
operations = [
@@ -4,29 +4,28 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0013_activate_fuzzystrmatch_extension"),
('core', '0013_activate_fuzzystrmatch_extension'),
]
operations = [
migrations.AddField(
model_name="document",
name="depth",
model_name='document',
name='depth',
field=models.PositiveIntegerField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name="document",
name="numchild",
model_name='document',
name='numchild',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name="document",
name="path",
model_name='document',
name='path',
# Allow null values pending the next datamigration to populate the field
field=models.CharField(
db_collation="C", max_length=252, null=True, unique=True
),
field=models.CharField(db_collation='C', max_length=252, null=True, unique=True),
preserve_default=False,
),
]
@@ -7,10 +7,9 @@ from treebeard.numconv import NumConv
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
STEPLEN = 7
def set_path_on_existing_documents(apps, schema_editor):
"""
Updates the `path` and `depth` fields for all existing Document records
Updates the `path` and `depth` fields for all existing Document records
to ensure valid materialized paths.
This function assigns a unique `path` to each Document as a root node
@@ -27,25 +26,27 @@ def set_path_on_existing_documents(apps, schema_editor):
updates = []
for i, pk in enumerate(documents):
key = numconv.int2str(i)
path = "{0}{1}".format(ALPHABET[0] * (STEPLEN - len(key)), key)
path = "{0}{1}".format(
ALPHABET[0] * (STEPLEN - len(key)),
key
)
updates.append(Document(pk=pk, path=path, depth=1))
# Bulk update using the prepared updates list
Document.objects.bulk_update(updates, ["depth", "path"])
Document.objects.bulk_update(updates, ['depth', 'path'])
class Migration(migrations.Migration):
dependencies = [
("core", "0014_add_tree_structure_to_documents"),
('core', '0014_add_tree_structure_to_documents'),
]
operations = [
migrations.RunPython(
set_path_on_existing_documents, reverse_code=migrations.RunPython.noop
),
migrations.RunPython(set_path_on_existing_documents, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name="document",
name="path",
field=models.CharField(db_collation="C", max_length=252, unique=True),
model_name='document',
name='path',
field=models.CharField(db_collation='C', max_length=252, unique=True),
),
]
@@ -4,27 +4,20 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0015_set_path_on_existing_documents"),
('core', '0015_set_path_on_existing_documents'),
]
operations = [
migrations.AddField(
model_name="document",
name="excerpt",
field=models.TextField(
blank=True, max_length=300, null=True, verbose_name="excerpt"
),
model_name='document',
name='excerpt',
field=models.TextField(blank=True, max_length=300, null=True, verbose_name='excerpt'),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
),
]
@@ -4,49 +4,33 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0016_add_document_excerpt"),
('core', '0016_add_document_excerpt'),
]
operations = [
migrations.AlterModelOptions(
name="document",
options={
"ordering": ("path",),
"verbose_name": "Document",
"verbose_name_plural": "Documents",
},
name='document',
options={'ordering': ('path',), 'verbose_name': 'Document', 'verbose_name_plural': 'Documents'},
),
migrations.AddField(
model_name="document",
name="ancestors_deleted_at",
model_name='document',
name='ancestors_deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="document",
name="deleted_at",
model_name='document',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
),
migrations.AddConstraint(
model_name="document",
constraint=models.CheckConstraint(
condition=models.Q(
("deleted_at__isnull", True),
("deleted_at", models.F("ancestors_deleted_at")),
_connector="OR",
),
name="check_deleted_at_matches_ancestors_deleted_at_when_set",
),
model_name='document',
constraint=models.CheckConstraint(condition=models.Q(('deleted_at__isnull', True), ('deleted_at', models.F('ancestors_deleted_at')), _connector='OR'), name='check_deleted_at_matches_ancestors_deleted_at_when_set'),
),
]
@@ -1,24 +0,0 @@
from django.db import migrations
def update_titles_to_null(apps, schema_editor):
"""
If the titles are "Untitled document" or "Unbenanntes Dokument" or "Document sans titre"
we set them to Null
"""
Document = apps.get_model("core", "Document")
Document.objects.filter(
title__in=["Untitled document", "Unbenanntes Dokument", "Document sans titre"]
).update(title=None)
class Migration(migrations.Migration):
dependencies = [
("core", "0017_add_fields_for_soft_delete"),
]
operations = [
migrations.RunPython(
update_titles_to_null, reverse_code=migrations.RunPython.noop
),
]
+2 -17
View File
@@ -629,9 +629,6 @@ class Document(MP_Node, BaseModel):
# which date to allow them anyway)
# Anonymous users should also not see document accesses
has_access_role = bool(roles) and not is_deleted
can_update_from_access = (
is_owner_or_admin or RoleChoices.EDITOR in roles
) and not is_deleted
# Add roles provided by the document link, taking into account its ancestors
@@ -650,23 +647,11 @@ class Document(MP_Node, BaseModel):
is_owner_or_admin or RoleChoices.EDITOR in roles
) and not is_deleted
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any(
[
ai_allow_reach_from == LinkReachChoices.PUBLIC and can_update,
ai_allow_reach_from == LinkReachChoices.AUTHENTICATED
and user.is_authenticated
and can_update,
ai_allow_reach_from == LinkReachChoices.RESTRICTED
and can_update_from_access,
]
)
return {
"accesses_manage": is_owner_or_admin,
"accesses_view": has_access_role,
"ai_transform": ai_access,
"ai_translate": ai_access,
"ai_transform": can_update,
"ai_translate": can_update,
"attachment_upload": can_update,
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
@@ -458,10 +458,6 @@ def test_api_document_invitations_create_email_from_content_language():
email_content = " ".join(email.body.split())
assert f"{user.full_name} a partagé un document avec vous!" in email_content
assert (
"Docs, votre nouvel outil incontournable pour organiser, partager et collaborer "
"sur vos documents en équipe." in email_content
)
def test_api_document_invitations_create_email_from_content_language_not_supported():
@@ -2,7 +2,6 @@
Test AI transform API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.core.cache import cache
@@ -32,9 +31,6 @@ def ai_settings():
yield
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
@@ -61,7 +57,6 @@ def test_api_documents_ai_transform_anonymous_forbidden(reach, role):
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_success(mock_create):
@@ -98,27 +93,6 @@ def test_api_documents_ai_transform_anonymous_success(mock_create):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content=answer))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = APIClient().post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
@@ -2,7 +2,6 @@
Test AI translate API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.core.cache import cache
@@ -52,9 +51,6 @@ def test_api_documents_ai_translate_viewset_options_metadata():
}
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
@@ -81,7 +77,6 @@ def test_api_documents_ai_translate_anonymous_forbidden(reach, role):
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_success(mock_create):
@@ -118,27 +113,6 @@ def test_api_documents_ai_translate_anonymous_success(mock_create):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content=answer))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = APIClient().post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
@@ -28,8 +28,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"attachment_upload": document.link_role == "editor",
"children_create": False,
"children_list": True,
@@ -84,8 +84,8 @@ def test_api_documents_retrieve_anonymous_public_parent():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"ai_transform": grand_parent.link_role == "editor",
"ai_translate": grand_parent.link_role == "editor",
"attachment_upload": grand_parent.link_role == "editor",
"children_create": False,
"children_list": True,
@@ -1,35 +0,0 @@
import pytest
from core import factories
@pytest.mark.django_db
def test_update_blank_title_migration(migrator):
"""
Test that the migration fixes the titles of documents that are
"Untitled document", "Unbenanntes Dokument" or "Document sans titre"
"""
migrator.apply_initial_migration(("core", "0017_add_fields_for_soft_delete"))
english_doc = factories.DocumentFactory(title="Untitled document")
german_doc = factories.DocumentFactory(title="Unbenanntes Dokument")
french_doc = factories.DocumentFactory(title="Document sans titre")
other_doc = factories.DocumentFactory(title="My document")
assert english_doc.title == "Untitled document"
assert german_doc.title == "Unbenanntes Dokument"
assert french_doc.title == "Document sans titre"
assert other_doc.title == "My document"
# Apply the migration
migrator.apply_tested_migration(("core", "0018_update_blank_title"))
english_doc.refresh_from_db()
german_doc.refresh_from_db()
french_doc.refresh_from_db()
other_doc.refresh_from_db()
assert english_doc.title == None
assert german_doc.title == None
assert french_doc.title == None
assert other_doc.title == "My document"
+10 -77
View File
@@ -12,7 +12,6 @@ from django.core import mail
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.test.utils import override_settings
from django.utils import timezone
import pytest
@@ -125,9 +124,6 @@ def test_models_documents_soft_delete(depth):
# get_abilities
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"is_authenticated,reach,role",
[
@@ -179,9 +175,6 @@ def test_models_documents_get_abilities_forbidden(
assert document.get_abilities(user) == expected_abilities
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"is_authenticated,reach",
[
@@ -250,8 +243,8 @@ def test_models_documents_get_abilities_editor(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": is_authenticated,
"ai_translate": is_authenticated,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"children_create": is_authenticated,
"children_list": True,
@@ -278,9 +271,6 @@ def test_models_documents_get_abilities_editor(
assert all(value is False for value in document.get_abilities(user).values())
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
def test_models_documents_get_abilities_owner(django_assert_num_queries):
"""Check abilities returned for the owner of a document."""
user = factories.UserFactory()
@@ -310,16 +300,12 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
}
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
expected_abilities["move"] = False
assert document.get_abilities(user) == expected_abilities
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
def test_models_documents_get_abilities_administrator(django_assert_num_queries):
"""Check abilities returned for the administrator of a document."""
user = factories.UserFactory()
@@ -349,15 +335,11 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
}
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"""Check abilities returned for the editor of a document."""
user = factories.UserFactory()
@@ -387,31 +369,23 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
}
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
@pytest.mark.parametrize("ai_access_setting", ["public", "authenticated", "restricted"])
def test_models_documents_get_abilities_reader_user(
ai_access_setting, django_assert_num_queries
):
def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
"""Check abilities returned for the reader of a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, "reader")])
access_from_link = (
document.link_reach != "restricted" and document.link_role == "editor"
)
expected_abilities = {
"accesses_manage": False,
"accesses_view": True,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"ai_transform": access_from_link,
"ai_translate": access_from_link,
"attachment_upload": access_from_link,
"children_create": access_from_link,
"children_list": True,
@@ -430,14 +404,11 @@ def test_models_documents_get_abilities_reader_user(
"versions_list": True,
"versions_retrieve": True,
}
with override_settings(AI_ALLOW_REACH_FROM=ai_access_setting):
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
@@ -475,44 +446,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_document_get_abilities_ai_access_authenticated(is_authenticated, reach):
"""Validate AI abilities when AI is available to any anonymous user with editor rights."""
user = factories.UserFactory() if is_authenticated else AnonymousUser()
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_transform"] is True
assert abilities["ai_translate"] is True
@override_settings(AI_ALLOW_REACH_FROM="authenticated")
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_document_get_abilities_ai_access_public(is_authenticated, reach):
"""Validate AI abilities when AI is available only to authenticated users with editor rights."""
user = factories.UserFactory() if is_authenticated else AnonymousUser()
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_transform"] == is_authenticated
assert abilities["ai_translate"] == is_authenticated
def test_models_documents_get_versions_slice_pagination(settings):
"""
The "get_versions_slice" method should allow navigating all versions of
+1 -6
View File
@@ -516,12 +516,7 @@ class Base(Configuration):
AI_API_KEY = values.Value(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_ALLOW_REACH_FROM = values.Value(
choices=("public", "authenticated", "restricted"),
default="authenticated",
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
AI_DOCUMENT_RATE_THROTTLE_RATES = {
"minute": 5,
"hour": 100,
+79 -141
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,23 +17,18 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr "Persönliche Daten"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr "Berechtigungen"
#: build/lib/core/admin.py:62 core/admin.py:62
#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr "Wichtige Daten"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr "Ersteller bin ich"
@@ -46,23 +41,23 @@ msgstr "Favorit"
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr ""
@@ -76,306 +71,270 @@ msgstr "Ungültiges Antwortformat oder Token-Verifizierung fehlgeschlagen"
msgid "User account is disabled"
msgstr "Benutzerkonto ist deaktiviert"
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
msgid "Reader"
msgstr "Lesen"
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
msgid "Editor"
msgstr "Bearbeiten"
#: build/lib/core/models.py:63 core/models.py:63
#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:64 core/models.py:64
#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr "Besitzer"
#: build/lib/core/models.py:75 core/models.py:75
#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr "Beschränkt"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr "Authentifiziert"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr "Öffentlich"
#: build/lib/core/models.py:103 core/models.py:103
#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
#: build/lib/core/models.py:104 core/models.py:104
#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:110 core/models.py:110
#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr "Erstellt"
#: build/lib/core/models.py:111 core/models.py:111
#: build/lib/core/models.py:120 core/models.py:120
msgid "date and time at which a record was created"
msgstr "Datum und Uhrzeit, an dem ein Datensatz erstellt wurde"
#: build/lib/core/models.py:116 core/models.py:116
#: build/lib/core/models.py:125 core/models.py:125
msgid "updated on"
msgstr "Aktualisiert"
#: build/lib/core/models.py:117 core/models.py:117
#: build/lib/core/models.py:126 core/models.py:126
msgid "date and time at which a record was last updated"
msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
#: build/lib/core/models.py:172 core/models.py:172
#: build/lib/core/models.py:181 core/models.py:181
msgid "sub"
msgstr "unter"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:192 core/models.py:192
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:198 core/models.py:198
#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:208 core/models.py:208
msgid "The language in which the user wants to see the interface."
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:210 core/models.py:210
#: build/lib/core/models.py:219 core/models.py:219
msgid "Whether the user is a device or a real user."
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
#: build/lib/core/models.py:213 core/models.py:213
#: build/lib/core/models.py:222 core/models.py:222
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:215 core/models.py:215
#: build/lib/core/models.py:224 core/models.py:224
msgid "Whether the user can log into this admin site."
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
#: build/lib/core/models.py:218 core/models.py:218
#: build/lib/core/models.py:227 core/models.py:227
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:221 core/models.py:221
#: build/lib/core/models.py:230 core/models.py:230
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
#: build/lib/core/models.py:233 core/models.py:233
#: build/lib/core/models.py:242 core/models.py:242
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:234 core/models.py:234
#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:719 core/models.py:719
#: build/lib/core/models.py:633 core/models.py:633
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:637 core/models.py:637
#, 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:726 core/models.py:726
#: build/lib/core/models.py:640 core/models.py:640
#, 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:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:821 core/models.py:821
#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:827 core/models.py:827
#: build/lib/core/models.py:670 core/models.py:670
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:850 core/models.py:850
#: build/lib/core/models.py:693 core/models.py:693
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:851 core/models.py:851
#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:700 core/models.py:700
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:879 core/models.py:879
#: build/lib/core/models.py:722 core/models.py:722
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:886 core/models.py:886
#: build/lib/core/models.py:729 core/models.py:729
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:892 core/models.py:892
#: build/lib/core/models.py:735 core/models.py:735
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
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:926 core/models.py:926
#: build/lib/core/models.py:759 core/models.py:759
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:927 core/models.py:927
#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:928 core/models.py:928
#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:932 core/models.py:932
#: build/lib/core/models.py:765 core/models.py:765
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:938 core/models.py:938
#: build/lib/core/models.py:771 core/models.py:771
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:939 core/models.py:939
#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1048 core/models.py:1048
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1049 core/models.py:1049
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1069 core/models.py:1069
#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
@@ -391,24 +350,3 @@ msgstr "Französisch"
msgid "German"
msgstr "Deutsch"
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "Logo-E-Mail"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Öffnen"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Erstellt von %(brandname)s "
+79 -141
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,23 +17,18 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:62 core/admin.py:62
#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
@@ -46,23 +41,23 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr ""
@@ -76,306 +71,270 @@ msgstr ""
msgid "User account is disabled"
msgstr ""
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:63 core/models.py:63
#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:64 core/models.py:64
#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:75 core/models.py:75
#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr ""
#: build/lib/core/models.py:103 core/models.py:103
#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
#: build/lib/core/models.py:104 core/models.py:104
#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:110 core/models.py:110
#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr ""
#: build/lib/core/models.py:111 core/models.py:111
#: build/lib/core/models.py:120 core/models.py:120
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:116 core/models.py:116
#: build/lib/core/models.py:125 core/models.py:125
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:117 core/models.py:117
#: build/lib/core/models.py:126 core/models.py:126
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:172 core/models.py:172
#: build/lib/core/models.py:181 core/models.py:181
msgid "sub"
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:192 core/models.py:192
msgid "full name"
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:198 core/models.py:198
#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:208 core/models.py:208
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr ""
#: build/lib/core/models.py:210 core/models.py:210
#: build/lib/core/models.py:219 core/models.py:219
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:213 core/models.py:213
#: build/lib/core/models.py:222 core/models.py:222
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:215 core/models.py:215
#: build/lib/core/models.py:224 core/models.py:224
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:218 core/models.py:218
#: build/lib/core/models.py:227 core/models.py:227
msgid "active"
msgstr ""
#: build/lib/core/models.py:221 core/models.py:221
#: build/lib/core/models.py:230 core/models.py:230
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:233 core/models.py:233
#: build/lib/core/models.py:242 core/models.py:242
msgid "user"
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr ""
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
msgid "title"
msgstr ""
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:719 core/models.py:719
#: build/lib/core/models.py:633 core/models.py:633
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:637 core/models.py:637
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:726 core/models.py:726
#: build/lib/core/models.py:640 core/models.py:640
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:821 core/models.py:821
#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:827 core/models.py:827
#: build/lib/core/models.py:670 core/models.py:670
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:850 core/models.py:850
#: build/lib/core/models.py:693 core/models.py:693
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:851 core/models.py:851
#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:700 core/models.py:700
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:722 core/models.py:722
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:886 core/models.py:886
#: build/lib/core/models.py:729 core/models.py:729
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:892 core/models.py:892
#: build/lib/core/models.py:735 core/models.py:735
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:926 core/models.py:926
#: build/lib/core/models.py:759 core/models.py:759
msgid "description"
msgstr ""
#: build/lib/core/models.py:927 core/models.py:927
#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr ""
#: build/lib/core/models.py:928 core/models.py:928
#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr ""
#: build/lib/core/models.py:932 core/models.py:932
#: build/lib/core/models.py:765 core/models.py:765
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:938 core/models.py:938
#: build/lib/core/models.py:771 core/models.py:771
msgid "Template"
msgstr ""
#: build/lib/core/models.py:939 core/models.py:939
#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1048 core/models.py:1048
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1049 core/models.py:1049
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1069 core/models.py:1069
#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr ""
@@ -391,24 +350,3 @@ msgstr ""
msgid "German"
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""
+79 -141
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,23 +17,18 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr "Infos Personnelles"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:62 core/admin.py:62
#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
@@ -46,23 +41,23 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
msgid "You have been granted ownership of a new document:"
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr ""
@@ -76,306 +71,270 @@ msgstr ""
msgid "User account is disabled"
msgstr ""
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
msgid "Reader"
msgstr "Lecteur"
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
msgid "Editor"
msgstr "Éditeur"
#: build/lib/core/models.py:63 core/models.py:63
#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr "Administrateur"
#: build/lib/core/models.py:64 core/models.py:64
#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr "Propriétaire"
#: build/lib/core/models.py:75 core/models.py:75
#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr "Restreint"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr "Authentifié"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr ""
#: build/lib/core/models.py:103 core/models.py:103
#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
#: build/lib/core/models.py:104 core/models.py:104
#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:110 core/models.py:110
#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr ""
#: build/lib/core/models.py:111 core/models.py:111
#: build/lib/core/models.py:120 core/models.py:120
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:116 core/models.py:116
#: build/lib/core/models.py:125 core/models.py:125
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:117 core/models.py:117
#: build/lib/core/models.py:126 core/models.py:126
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:172 core/models.py:172
#: build/lib/core/models.py:181 core/models.py:181
msgid "sub"
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:192 core/models.py:192
msgid "full name"
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:198 core/models.py:198
#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:208 core/models.py:208
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr ""
#: build/lib/core/models.py:210 core/models.py:210
#: build/lib/core/models.py:219 core/models.py:219
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:213 core/models.py:213
#: build/lib/core/models.py:222 core/models.py:222
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:215 core/models.py:215
#: build/lib/core/models.py:224 core/models.py:224
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:218 core/models.py:218
#: build/lib/core/models.py:227 core/models.py:227
msgid "active"
msgstr ""
#: build/lib/core/models.py:221 core/models.py:221
#: build/lib/core/models.py:230 core/models.py:230
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:233 core/models.py:233
#: build/lib/core/models.py:242 core/models.py:242
msgid "user"
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr ""
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
msgid "title"
msgstr ""
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:719 core/models.py:719
#: build/lib/core/models.py:633 core/models.py:633
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:637 core/models.py:637
#, 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:726 core/models.py:726
#: build/lib/core/models.py:640 core/models.py:640
#, 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:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:821 core/models.py:821
#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:827 core/models.py:827
#: build/lib/core/models.py:670 core/models.py:670
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:850 core/models.py:850
#: build/lib/core/models.py:693 core/models.py:693
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:851 core/models.py:851
#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:700 core/models.py:700
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:722 core/models.py:722
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:886 core/models.py:886
#: build/lib/core/models.py:729 core/models.py:729
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:892 core/models.py:892
#: build/lib/core/models.py:735 core/models.py:735
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:926 core/models.py:926
#: build/lib/core/models.py:759 core/models.py:759
msgid "description"
msgstr ""
#: build/lib/core/models.py:927 core/models.py:927
#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr ""
#: build/lib/core/models.py:928 core/models.py:928
#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr ""
#: build/lib/core/models.py:932 core/models.py:932
#: build/lib/core/models.py:765 core/models.py:765
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:938 core/models.py:938
#: build/lib/core/models.py:771 core/models.py:771
msgid "Template"
msgstr ""
#: build/lib/core/models.py:939 core/models.py:939
#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1048 core/models.py:1048
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1049 core/models.py:1049
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1069 core/models.py:1069
#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr ""
@@ -391,24 +350,3 @@ msgstr ""
msgid "German"
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "Logo de l'e-mail"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Ouvrir"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Proposé par %(brandname)s "
+79 -141
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,23 +17,18 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:62 core/admin.py:62
#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
@@ -46,23 +41,23 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr ""
@@ -76,306 +71,270 @@ msgstr ""
msgid "User account is disabled"
msgstr ""
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:63 core/models.py:63
#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:64 core/models.py:64
#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:75 core/models.py:75
#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr ""
#: build/lib/core/models.py:103 core/models.py:103
#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
#: build/lib/core/models.py:104 core/models.py:104
#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:110 core/models.py:110
#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr ""
#: build/lib/core/models.py:111 core/models.py:111
#: build/lib/core/models.py:120 core/models.py:120
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:116 core/models.py:116
#: build/lib/core/models.py:125 core/models.py:125
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:117 core/models.py:117
#: build/lib/core/models.py:126 core/models.py:126
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:162 core/models.py:162
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:166 core/models.py:166
#: build/lib/core/models.py:175 core/models.py:175
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:172 core/models.py:172
#: build/lib/core/models.py:181 core/models.py:181
msgid "sub"
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:183 core/models.py:183
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:192 core/models.py:192
msgid "full name"
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:198 core/models.py:198
#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:208 core/models.py:208
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr ""
#: build/lib/core/models.py:210 core/models.py:210
#: build/lib/core/models.py:219 core/models.py:219
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:213 core/models.py:213
#: build/lib/core/models.py:222 core/models.py:222
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:215 core/models.py:215
#: build/lib/core/models.py:224 core/models.py:224
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:218 core/models.py:218
#: build/lib/core/models.py:227 core/models.py:227
msgid "active"
msgstr ""
#: build/lib/core/models.py:221 core/models.py:221
#: build/lib/core/models.py:230 core/models.py:230
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:233 core/models.py:233
#: build/lib/core/models.py:242 core/models.py:242
msgid "user"
msgstr ""
#: build/lib/core/models.py:234 core/models.py:234
#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr ""
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
msgid "title"
msgstr ""
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:719 core/models.py:719
#: build/lib/core/models.py:633 core/models.py:633
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:637 core/models.py:637
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:726 core/models.py:726
#: build/lib/core/models.py:640 core/models.py:640
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:821 core/models.py:821
#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:827 core/models.py:827
#: build/lib/core/models.py:670 core/models.py:670
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:850 core/models.py:850
#: build/lib/core/models.py:693 core/models.py:693
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:851 core/models.py:851
#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:700 core/models.py:700
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:722 core/models.py:722
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:886 core/models.py:886
#: build/lib/core/models.py:729 core/models.py:729
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:892 core/models.py:892
#: build/lib/core/models.py:735 core/models.py:735
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:926 core/models.py:926
#: build/lib/core/models.py:759 core/models.py:759
msgid "description"
msgstr ""
#: build/lib/core/models.py:927 core/models.py:927
#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr ""
#: build/lib/core/models.py:928 core/models.py:928
#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr ""
#: build/lib/core/models.py:932 core/models.py:932
#: build/lib/core/models.py:765 core/models.py:765
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:938 core/models.py:938
#: build/lib/core/models.py:771 core/models.py:771
msgid "Template"
msgstr ""
#: build/lib/core/models.py:939 core/models.py:939
#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1048 core/models.py:1048
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1049 core/models.py:1049
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1069 core/models.py:1069
#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr ""
@@ -391,24 +350,3 @@ msgstr ""
msgid "German"
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""
+2 -2
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "2.2.0"
version = "2.0.1"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -68,7 +68,6 @@ dependencies = [
[project.optional-dependencies]
dev = [
"django-extensions==3.2.3",
"django-test-migrations==1.4.0",
"drf-spectacular-sidecar==2024.12.1",
"freezegun==1.5.1",
"ipdb==0.13.13",
@@ -100,6 +99,7 @@ exclude = [
"build",
"venv",
"__pycache__",
"*/migrations/*",
]
line-length = 88
@@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
await expect(
page.locator('header').first().locator('h2').getByText('Docs'),
).toBeVisible();
@@ -24,6 +24,6 @@ test.describe('404', () => {
page,
}) => {
await page.getByText('Back to home page').click();
await expect(page).toHaveURL('/');
await expect(page).toHaveURL('/docs/');
});
});
@@ -1,46 +1,30 @@
import { FullConfig, FullProject, chromium, expect } from '@playwright/test';
import { test as setup } from '@playwright/test';
import { keyCloakSignIn } from './common';
const saveStorageState = async (
browserConfig: FullProject<unknown, unknown>,
) => {
const browserName = browserConfig?.name || 'chromium';
setup('authenticate-chromium', async ({ page }) => {
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, 'chromium');
await page
.context()
.storageState({ path: `playwright/.auth/user-chromium.json` });
});
const { storageState, ...useConfig } = browserConfig?.use;
const browser = await chromium.launch();
const context = await browser.newContext(useConfig);
const page = await context.newPage();
setup('authenticate-webkit', async ({ page }) => {
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, 'webkit');
await page
.context()
.storageState({ path: `playwright/.auth/user-webkit.json` });
});
await page.goto('/', { waitUntil: 'networkidle' });
await page.content();
await expect(page.getByText('Docs').first()).toBeVisible();
await keyCloakSignIn(page, browserName);
await expect(
page.locator('header').first().getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
await page.context().storageState({
path: storageState as string,
});
await browser.close();
};
async function globalSetup(config: FullConfig) {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const chromeConfig = config.projects.find((p) => p.name === 'chromium')!;
const firefoxConfig = config.projects.find((p) => p.name === 'firefox')!;
const webkitConfig = config.projects.find((p) => p.name === 'webkit')!;
/* eslint-enable @typescript-eslint/no-non-null-assertion */
await saveStorageState(chromeConfig);
await saveStorageState(webkitConfig);
await saveStorageState(firefoxConfig);
}
export default globalSetup;
setup('authenticate-firefox', async ({ page }) => {
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, 'firefox');
await page
.context()
.storageState({ path: `playwright/.auth/user-firefox.json` });
});
@@ -1,17 +1,6 @@
import { Page, expect } from '@playwright/test';
export const keyCloakSignIn = async (
page: Page,
browserName: string,
fromHome: boolean = true,
) => {
if (fromHome) {
await page
.getByRole('button', { name: 'Proconnect Login' })
.first()
.click();
}
export const keyCloakSignIn = async (page: Page, browserName: string) => {
const login = `user-e2e-${browserName}`;
const password = `password-e2e-${browserName}`;
@@ -269,8 +258,3 @@ export const mockedAccesses = async (page: Page, json?: object) => {
}
});
};
export const expectLoginPage = async (page: Page) =>
await expect(
page.getByRole('heading', { name: 'Collaborative writing' }),
).toBeVisible();
@@ -21,13 +21,43 @@ const config = {
};
test.describe('Config', () => {
test('it checks that media server is configured from config endpoint', async ({
page,
browserName,
}) => {
await page.goto('/docs/');
await createDoc(page, 'doc-media', browserName, 1);
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('.bn-block-outer').last().fill('Anything');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
await expect(image).toBeVisible();
// Check src of image
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
});
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('/');
await page.goto('/docs/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
@@ -58,41 +88,11 @@ test.describe('Config', () => {
predicate: (msg) => msg.text().includes(invalidMsg),
});
await page.goto('/');
await page.goto('/docs/');
expect((await consoleMessage).text()).toContain(invalidMsg);
});
test('it checks that media server is configured from config endpoint', async ({
page,
browserName,
}) => {
await page.goto('/');
await createDoc(page, 'doc-media', browserName, 1);
const fileChooserPromise = page.waitForEvent('filechooser');
await page.locator('.bn-block-outer').last().fill('Anything');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
await expect(image).toBeVisible();
// Check src of image
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
});
test('it checks that collaboration server is configured from config endpoint', async ({
page,
browserName,
@@ -101,7 +101,7 @@ test.describe('Config', () => {
return webSocket.url().includes('ws://localhost:8083/collaboration/ws/');
});
await page.goto('/');
await page.goto('/docs');
const randomDoc = await createDoc(
page,
@@ -133,7 +133,7 @@ test.describe('Config', () => {
}
});
await page.goto('/');
await page.goto('/docs/');
await expect(
page.locator('#crisp-chatbox').getByText('Invalid website'),
@@ -141,9 +141,7 @@ test.describe('Config', () => {
});
});
test.describe('Config: Not loggued', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test.describe('Config footer logo', () => {
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
@@ -152,16 +150,12 @@ test.describe('Config: Not loggued', () => {
response.url().includes('/config/') && response.status() === 200,
);
await page.goto('/');
await page.goto('/docs/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const jsonResponse = await response.json();
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('dsfr');
const footer = page.locator('footer').first();
// alt 'Gouvernement Logo' comes from the theme
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
});
});
@@ -9,7 +9,7 @@ import {
} from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Doc Create', () => {
@@ -24,6 +24,8 @@ test.describe('Doc Create', () => {
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
await expect(page.getByTestId('grid-loader')).toBeVisible();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
@@ -60,7 +62,7 @@ test.describe('Doc Create: Not loggued', () => {
);
expect(newDoc.ok()).toBeTruthy();
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
await goToGridDoc(page, { title });
@@ -1,5 +1,3 @@
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
import path from 'path';
import { expect, test } from '@playwright/test';
@@ -12,7 +10,7 @@ import {
} from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Doc Editor', () => {
@@ -259,7 +257,7 @@ test.describe('Doc Editor', () => {
await editor.fill('Hello World Doc persisted 2');
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
await page.goto('/');
await page.goto('/docs/');
await goToGridDoc(page, {
title: doc,
@@ -370,77 +368,4 @@ test.describe('Doc Editor', () => {
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'public',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
});
await goToGridDoc(page);
await verifyDocName(page, 'Mocked document');
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').dblclick();
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI' }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
});
@@ -7,7 +7,7 @@ import pdf from 'pdf-parse';
import { createDoc, verifyDocName } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Doc Export', () => {
@@ -29,7 +29,9 @@ test.describe('Doc Export', () => {
.first(),
).toBeVisible();
await expect(
page.getByText('Download your document in a .docx or .pdf format.'),
page.getByText(
'Upload your docs to a Microsoft Word, Open Office or PDF document',
),
).toBeVisible();
await expect(
page.getByRole('combobox', { name: 'Template' }),
@@ -41,16 +43,8 @@ test.describe('Doc Export', () => {
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
});
test('it exports the doc with pdf line break', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(
page,
'doc-editor-line-break',
browserName,
1,
);
test('it exports the doc to pdf', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
@@ -58,20 +52,8 @@ test.describe('Doc Export', () => {
await verifyDocName(page, randomDoc);
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.locator('.bn-block-outer').last().fill('Hello');
await page.keyboard.press('Enter');
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Page Break').click();
await expect(editor.locator('.bn-page-break')).toBeVisible();
await page.keyboard.press('Enter');
await editor.locator('.bn-block-outer').last().fill('World');
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await page
.getByRole('button', {
@@ -89,10 +71,9 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfData = await pdf(pdfBuffer);
const pdfText = (await pdf(pdfBuffer)).text;
expect(pdfData.numpages).toBe(2);
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
expect(pdfText).toContain('Hello World'); // This is the doc text
});
test('it exports the doc to docx', async ({ page, browserName }) => {
@@ -114,7 +95,7 @@ test.describe('Doc Export', () => {
.click();
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
await page.getByRole('option', { name: 'Word / Open Office' }).click();
await page
.getByRole('button', {
@@ -8,14 +8,14 @@ type SmallDoc = {
};
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Documents Grid mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('it checks the grid when mobile', async ({ page }) => {
@@ -76,7 +76,7 @@ test.describe('Documents Grid mobile', () => {
}
});
await page.goto('/');
await page.goto('/docs/');
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
@@ -195,7 +195,7 @@ test.describe('Document grid item options', () => {
},
});
});
await page.goto('/');
await page.goto('/docs/');
const button = page.getByTestId(
`docs-grid-actions-button-mocked-document-id`,
@@ -213,6 +213,7 @@ test.describe('Document grid item options', () => {
test.describe('Documents filters', () => {
test('it checks the prebuild left panel filters', async ({ page }) => {
// All Docs
await expect(page.getByTestId('grid-loader')).toBeVisible();
const response = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1') &&
@@ -253,6 +254,7 @@ test.describe('Documents filters', () => {
url = new URL(page.url());
target = url.searchParams.get('target');
expect(target).toBe('my_docs');
await expect(page.getByTestId('grid-loader')).toBeVisible();
const responseMyDocs = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
@@ -268,6 +270,7 @@ test.describe('Documents filters', () => {
url = new URL(page.url());
target = url.searchParams.get('target');
expect(target).toBe('shared_with_me');
await expect(page.getByTestId('grid-loader')).toBeVisible();
const responseSharedWithMe = await page.waitForResponse(
(response) =>
response.url().includes('documents/?page=1&is_creator_me=false') &&
@@ -283,11 +286,13 @@ test.describe('Documents filters', () => {
test.describe('Documents Grid', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('checks all the elements are visible', async ({ page }) => {
let docs: SmallDoc[] = [];
await expect(page.getByTestId('grid-loader')).toBeVisible();
const response = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1') &&
@@ -10,7 +10,7 @@ import {
} from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Doc Header', () => {
@@ -395,7 +395,9 @@ test.describe('Doc Header', () => {
navigator.clipboard.readText(),
);
const clipboardContent = await handle.jsonValue();
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
expect(clipboardContent.trim()).toBe(
`<h1 data-level=\"1\">Hello World</h1><p></p>`,
);
});
test('it checks the copy link button', async ({ page }) => {
@@ -432,7 +434,7 @@ test.describe('Documents Header mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('it checks the copy link button', async ({ page, browserName }) => {
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { createDoc, randomName } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Document create member', () => {
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { addNewMember, createDoc, goToGridDoc, verifyDocName } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Document list members', () => {
@@ -1,10 +1,10 @@
import { expect, test } from '@playwright/test';
import { expectLoginPage, keyCloakSignIn, mockedDocument } from './common';
import { keyCloakSignIn, mockedDocument } from './common';
test.describe('Doc Routing', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('Check the presence of the meta tag noindex', async ({ page }) => {
@@ -24,7 +24,7 @@ test.describe('Doc Routing', () => {
});
test('checks alias docs url with homepage', async ({ page }) => {
await expect(page).toHaveURL('/');
await expect(page).toHaveURL('/docs/');
const buttonCreateHomepage = page.getByRole('button', {
name: 'New doc',
@@ -62,14 +62,18 @@ test.describe('Doc Routing: Not loggued', () => {
await mockedDocument(page, { link_reach: 'public' });
await page.goto('/docs/mocked-document-id/');
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await page.getByRole('button', { name: 'Login' }).click();
await keyCloakSignIn(page, browserName, false);
await keyCloakSignIn(page, browserName);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
});
// eslint-disable-next-line playwright/expect-expect
test('The homepage redirects to login.', async ({ page }) => {
await page.goto('/');
await expectLoginPage(page);
test('Check is redirected to home page if user is not logged', async ({
page,
}) => {
await page.goto('/docs/');
await expect(page.getByTestId('proconnect-button').first()).toBeVisible();
});
});
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { createDoc, verifyDocName } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Document search', () => {
@@ -15,7 +15,7 @@ test.describe('Document search', () => {
1,
);
await verifyDocName(page, doc1Title);
await page.goto('/');
await page.goto('/docs/');
const [doc2Title] = await createDoc(
page,
@@ -24,7 +24,7 @@ test.describe('Document search', () => {
1,
);
await verifyDocName(page, doc2Title);
await page.goto('/');
await page.goto('/docs/');
await page.getByRole('button', { name: 'search' }).click();
await expect(
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { createDoc, verifyDocName } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Doc Table Content', () => {
@@ -8,7 +8,7 @@ import {
} from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Doc Version', () => {
@@ -1,17 +1,12 @@
import { expect, test } from '@playwright/test';
import {
createDoc,
expectLoginPage,
keyCloakSignIn,
verifyDocName,
} from './common';
import { createDoc, keyCloakSignIn, verifyDocName } from './common';
const browsersName = ['chromium', 'webkit', 'firefox'];
test.describe('Doc Visibility', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('It checks the copy link button', async ({ page, browserName }) => {
@@ -76,7 +71,8 @@ test.describe('Doc Visibility: Restricted', () => {
page,
browserName,
}) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
@@ -96,9 +92,9 @@ test.describe('Doc Visibility: Restricted', () => {
})
.click();
await expectLoginPage(page);
await expect(page.getByTestId('proconnect-button').first()).toBeVisible();
await page.goto(urlDoc);
await page.getByTestId('proconnect-button').first().click();
await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible();
});
@@ -107,7 +103,8 @@ test.describe('Doc Visibility: Restricted', () => {
page,
browserName,
}) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
@@ -124,12 +121,9 @@ test.describe('Doc Visibility: Restricted', () => {
const otherBrowser = browsersName.find((b) => b !== browserName);
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
await expect(
@@ -139,7 +133,8 @@ test.describe('Doc Visibility: Restricted', () => {
test('A doc is accessible when member.', async ({ page, browserName }) => {
test.slow();
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
@@ -176,14 +171,14 @@ test.describe('Doc Visibility: Restricted', () => {
})
.click();
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await verifyDocName(page, docTitle);
await expect(page.getByLabel('Share button')).toBeVisible();
});
@@ -196,7 +191,8 @@ test.describe('Doc Visibility: Public', () => {
page,
browserName,
}) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
@@ -257,7 +253,7 @@ test.describe('Doc Visibility: Public', () => {
})
.click();
await expectLoginPage(page);
await expect(page.getByTestId('proconnect-button').first()).toBeVisible();
await page.goto(urlDoc);
@@ -275,7 +271,8 @@ test.describe('Doc Visibility: Public', () => {
page,
browserName,
}) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
@@ -323,7 +320,7 @@ test.describe('Doc Visibility: Public', () => {
})
.click();
await expectLoginPage(page);
await expect(page.getByTestId('proconnect-button').first()).toBeVisible();
await page.goto(urlDoc);
@@ -339,7 +336,8 @@ test.describe('Doc Visibility: Authenticated', () => {
page,
browserName,
}) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
@@ -374,11 +372,12 @@ test.describe('Doc Visibility: Authenticated', () => {
})
.click();
await expectLoginPage(page);
await expect(page.getByTestId('proconnect-button').first()).toBeVisible();
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeHidden();
await page.getByTestId('proconnect-button').first().click();
await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible();
});
@@ -386,7 +385,8 @@ test.describe('Doc Visibility: Authenticated', () => {
page,
browserName,
}) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
@@ -422,12 +422,9 @@ test.describe('Doc Visibility: Authenticated', () => {
.click();
const otherBrowser = browsersName.find((b) => b !== browserName);
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
@@ -440,7 +437,8 @@ test.describe('Doc Visibility: Authenticated', () => {
page,
browserName,
}) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
@@ -482,12 +480,9 @@ test.describe('Doc Visibility: Authenticated', () => {
.click();
const otherBrowser = browsersName.find((b) => b !== browserName);
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
@@ -1,10 +1,14 @@
import { expect, test } from '@playwright/test';
import { keyCloakSignIn } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/docs/');
});
test.describe('Footer', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer').first();
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
@@ -44,6 +48,17 @@ test.describe('Footer', () => {
).toBeVisible();
});
test('checks footer is not visible on doc editor', async ({
page,
browserName,
}) => {
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
await page.goto('/docs/');
await expect(page.locator('footer')).toBeHidden();
});
const legalPages = [
{ name: 'Legal Notice', url: '/legal-notice/' },
{ name: 'Personal data and cookies', url: '/personal-data-cookies/' },
@@ -51,8 +66,6 @@ test.describe('Footer', () => {
];
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();
@@ -1,16 +1,16 @@
import { expect, test } from '@playwright/test';
import { expectLoginPage, keyCloakSignIn } from './common';
import { keyCloakSignIn } from './common';
test.describe('Header', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('checks all the elements are visible', async ({ page }) => {
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.getByAltText('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'color',
'rgb(0, 0, 145)',
@@ -69,7 +69,7 @@ test.describe('Header mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('it checks the header when mobile', async ({ page }) => {
@@ -88,9 +88,9 @@ test.describe('Header mobile', () => {
test.describe('Header: Log out', () => {
test.use({ storageState: { cookies: [], origins: [] } });
// eslint-disable-next-line playwright/expect-expect
test('checks logout button', async ({ page, browserName }) => {
await page.goto('/');
await page.goto('/docs/');
await page.getByTestId('proconnect-button').first().click();
await keyCloakSignIn(page, browserName);
await page
@@ -99,6 +99,6 @@ test.describe('Header: Log out', () => {
})
.click();
await expectLoginPage(page);
await expect(page.getByTestId('proconnect-button')).toHaveCount(2);
});
});
@@ -20,33 +20,30 @@ test.describe('Home page', () => {
await expect(
header.getByRole('img', { name: 'Gouvernement Logo' }),
).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(
header.getByRole('img', { name: 'Docs app logo' }),
).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
await expect(header.getByText('BETA')).toBeVisible();
// Check the titles
const h2 = page.locator('h2');
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();
// Check the ttile and subtitle are visible
await expect(page.getByText('Collaborative writing made')).toBeVisible();
await expect(page.getByText('Collaborate and write in real')).toBeVisible();
await expect(page.getByText('An uncompromising writing')).toBeVisible();
await expect(page.getByText('Docs offers an intuitive')).toBeVisible();
await expect(page.getByText('Simple and secure')).toBeVisible();
await expect(page.getByText('Docs makes real-time')).toBeVisible();
await expect(page.getByText('Flexible export.')).toBeVisible();
await expect(page.getByText('To facilitate the circulation')).toBeVisible();
await expect(page.getByText('A new way to organize')).toBeVisible();
await expect(page.getByText('Docs transforms your')).toBeVisible();
await expect(
page.getByText('Docs is already available, log in to use it now.'),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Proconnect Login' }),
).toHaveCount(2);
await expect(page.getByTestId('proconnect-button')).toHaveCount(2);
// Footer - The footer is already tested in its entirety in the footer.spec.ts file
await expect(footer).toBeVisible();
await expect(
page.getByRole('link', { name: 'expand_more See more' }),
).toBeVisible();
});
});
@@ -1,7 +1,7 @@
import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test.describe('Language', () => {
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
test.describe('Left panel desktop', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('checks all the elements are visible', async ({ page }) => {
@@ -17,7 +17,7 @@ test.describe('Left panel mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.goto('/docs/');
});
test('checks all the desktop elements are hidden and all mobile elements are visible', async ({
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "2.2.0",
"version": "2.0.1",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.50.1",
"@playwright/test": "1.49.1",
"@types/node": "*",
"@types/pdf-parse": "1.1.4",
"eslint-config-impress": "*",
+5 -1
View File
@@ -38,9 +38,10 @@ export default defineConfig({
timeout: 120 * 1000,
reuseExistingServer: true,
},
globalSetup: require.resolve('./__tests__/app-impress/auth.setup'),
/* Configure projects for major browsers */
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
@@ -52,6 +53,7 @@ export default defineConfig({
permissions: ['clipboard-read', 'clipboard-write'],
},
},
dependencies: ['setup'],
},
{
name: 'webkit',
@@ -61,6 +63,7 @@ export default defineConfig({
timezoneId: 'Europe/Paris',
storageState: 'playwright/.auth/user-webkit.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
@@ -76,6 +79,7 @@ export default defineConfig({
},
},
},
dependencies: ['setup'],
},
],
});
-6
View File
@@ -253,9 +253,6 @@ const config = {
'la-gauffre': {
activated: false,
},
'home-proconnect': {
activated: false,
},
},
},
dsfr: {
@@ -471,9 +468,6 @@ const config = {
'la-gauffre': {
activated: true,
},
'home-proconnect': {
activated: true,
},
},
},
},
+27 -28
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "2.2.0",
"version": "2.0.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -15,49 +15,48 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.1",
"@blocknote/core": "0.23.2",
"@blocknote/mantine": "0.23.2",
"@blocknote/react": "0.23.2",
"@blocknote/xl-docx-exporter": "0.23.2",
"@blocknote/xl-pdf-exporter": "0.23.2",
"@blocknote/core": "0.21.0",
"@blocknote/mantine": "0.21.0",
"@blocknote/react": "0.21.0",
"@blocknote/xl-docx-exporter": "0.21.0",
"@blocknote/xl-pdf-exporter": "0.21.0",
"@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.15.2",
"@hocuspocus/provider": "2.15.0",
"@openfun/cunningham-react": "2.9.4",
"@react-pdf/renderer": "4.1.6",
"@sentry/nextjs": "8.54.0",
"@tanstack/react-query": "5.66.0",
"@sentry/nextjs": "8.47.0",
"@tanstack/react-query": "5.62.11",
"cmdk": "1.0.4",
"crisp-sdk-web": "1.0.25",
"docx": "9.1.1",
"i18next": "24.2.2",
"docx": "9.1.0",
"i18next": "24.2.0",
"i18next-browser-languagedetector": "8.0.2",
"idb": "8.0.2",
"idb": "8.0.1",
"lodash": "4.17.21",
"luxon": "3.5.0",
"next": "15.1.6",
"posthog-js": "1.215.6",
"next": "15.1.3",
"posthog-js": "1.204.0",
"react": "*",
"react-aria-components": "1.6.0",
"react-aria-components": "1.5.0",
"react-dom": "*",
"react-i18next": "15.4.0",
"react-intersection-observer": "9.15.1",
"react-select": "5.10.0",
"styled-components": "6.1.15",
"react-intersection-observer": "9.13.1",
"react-select": "5.9.0",
"styled-components": "6.1.13",
"use-debounce": "10.0.4",
"y-protocols": "1.0.6",
"yjs": "13.6.23",
"zustand": "5.0.3"
"yjs": "13.6.21",
"zustand": "5.0.2"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.66.0",
"@tanstack/react-query-devtools": "5.62.11",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
"@testing-library/react": "16.1.0",
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.15",
"@types/lodash": "4.17.13",
"@types/luxon": "3.4.2",
"@types/node": "*",
"@types/react": "18.3.12",
@@ -70,9 +69,9 @@
"jest-environment-jsdom": "29.7.0",
"node-fetch": "2.7.0",
"prettier": "3.4.2",
"stylelint": "16.14.1",
"stylelint-config-standard": "37.0.0",
"stylelint-prettier": "5.0.3",
"stylelint": "16.12.0",
"stylelint-config-standard": "36.0.1",
"stylelint-prettier": "5.0.2",
"typescript": "*",
"webpack": "5.97.1",
"workbox-webpack-plugin": "7.1.0"
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

@@ -0,0 +1,4 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.2515 62.7264C49.7721 62.0196 52.1861 60.7298 53.7701 58.4505C55.3337 56.2187 55.774 53.3875 55.774 50.6632V10.749C55.774 10.0423 55.7356 9.33431 55.6503 8.63477C56.9023 9.12719 57.8672 9.94087 58.5451 11.0758C59.3448 12.3663 59.7447 14.0657 59.7447 16.1741V56.7153C59.7447 59.5689 59.0449 61.7046 57.6454 63.1223C56.2458 64.54 54.1374 65.2489 51.3202 65.2489H36.0065C36.3692 65.1854 36.7328 65.12 37.0972 65.0528C40.2928 64.4817 43.6701 63.7067 47.2256 62.7336L47.2515 62.7264Z" fill="#C9191E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4521 55.8703V15.2746C10.4521 12.8027 11.1156 10.9306 12.4424 9.65832C13.7874 8.38601 15.5686 7.68624 17.7861 7.55901C21.0577 7.35908 24.1567 7.08644 27.083 6.7411C30.0093 6.37758 32.8084 5.95954 35.4803 5.48697C38.1703 5.01439 40.7876 4.48729 43.3322 3.90567C45.9495 3.28769 47.958 3.59668 49.3575 4.83264C50.757 6.06859 51.4568 8.04067 51.4568 10.7489V50.663C51.4568 53.044 51.0479 54.8161 50.2299 55.9794C49.412 57.1608 48.0307 58.0242 46.0859 58.5695C42.6324 59.5146 39.379 60.2598 36.3254 60.8051C33.2719 61.3685 30.2911 61.7957 27.3829 62.0865C24.4748 62.3773 21.494 62.6045 18.4404 62.7681C15.914 62.9135 13.951 62.3864 12.5515 61.1868C11.1519 60.0053 10.4521 58.2332 10.4521 55.8703ZM20.4387 22.6454C24.4424 22.3905 28.1112 21.9992 31.4447 21.4709C32.2535 21.339 33.0611 21.2017 33.8657 21.0592C34.7076 20.9101 35.3165 20.1764 35.3165 19.3233C35.3165 18.2185 34.3166 17.3865 33.2308 17.5717C32.6071 17.6781 31.9801 17.7815 31.3497 17.8819C28.0496 18.4078 24.4029 18.7979 20.4098 19.052C19.8118 19.0906 19.336 19.2769 19.0302 19.6482C18.741 19.9994 18.5993 20.4218 18.5993 20.9033C18.5993 21.3928 18.7644 21.8188 19.0941 22.1691L19.0987 22.1737C19.456 22.531 19.91 22.6857 20.4387 22.6454ZM20.4374 31.6973C24.4416 31.4424 28.1108 31.051 31.4447 30.5227C34.7931 29.9768 38.1061 29.3397 41.3818 28.6117C42.0465 28.464 42.5444 28.2314 42.7935 27.8701C43.0383 27.5228 43.1594 27.1247 43.1594 26.6834C43.1594 26.1857 42.9698 25.757 42.601 25.4087C42.2038 25.0336 41.6486 24.9462 40.9915 25.0815L40.9873 25.0825C37.8658 25.7902 34.6533 26.4074 31.3497 26.9337C28.0496 27.4596 24.4029 27.8497 20.4098 28.1038C19.8118 28.1424 19.336 28.3287 19.0302 28.7C18.7424 29.0495 18.5993 29.4621 18.5993 29.9278C18.5993 30.4329 18.7625 30.8691 19.0939 31.2211L19.1034 31.2301C19.4595 31.5653 19.9088 31.7177 20.432 31.6976L20.4374 31.6973ZM20.4383 40.7488C24.4422 40.4758 28.111 40.0753 31.4446 39.547C34.7933 39.0192 38.1057 38.3913 41.3818 37.6633C42.0481 37.5152 42.5464 37.2729 42.7951 36.892C43.0376 36.5471 43.1594 36.1594 43.1594 35.735C43.1594 35.2373 42.9698 34.8086 42.601 34.4602C42.2038 34.0852 41.6486 33.9978 40.9915 34.1331L40.9885 34.1338C37.8666 34.8235 34.6537 35.4316 31.3497 35.958C28.0493 36.4839 24.4025 36.8832 20.4092 37.1554C19.8115 37.1941 19.3359 37.3804 19.0302 37.7516C18.7424 38.1011 18.5993 38.5137 18.5993 38.9794C18.5993 39.4845 18.7625 39.9206 19.0939 40.2727L19.1034 40.2816C19.4595 40.6168 19.9088 40.7693 20.432 40.7492L20.4383 40.7488ZM31.4447 48.5358C28.1112 49.0641 24.4424 49.4555 20.4388 49.7103C19.91 49.7506 19.456 49.596 19.0987 49.2386L19.0941 49.2339C18.7645 48.8837 18.5993 48.4577 18.5993 47.9682C18.5993 47.4867 18.741 47.0644 19.0302 46.7132C19.336 46.3418 19.812 46.1555 20.41 46.117C24.4031 45.8629 28.0496 45.4727 31.3497 44.9469C31.9801 44.8464 32.6071 44.743 33.2308 44.6366C34.3166 44.4514 35.3165 45.2834 35.3165 46.3882C35.3165 47.2413 34.7076 47.975 33.8657 48.1241C33.0611 48.2666 32.2535 48.404 31.4447 48.5358Z" fill="#000091"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -1,12 +0,0 @@
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
fill="#C9191E"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
fill="#000091"
/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useCunninghamTheme } from '@/cunningham';
import { Auth } from '@/features/auth';
import '@/i18n/initI18n';
import { useResponsiveStore } from '@/stores/';
import { Auth } from './auth/';
import { ConfigProvider } from './config/';
/**
@@ -0,0 +1,53 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { PropsWithChildren, useEffect, useState } from 'react';
import { Box } from '@/components';
import { useAuthStore } from './useAuthStore';
/**
* TODO: Remove this restriction when we will have a homepage design for non-authenticated users.
*
* We define the paths that are not allowed without authentication.
* Actually, only the home page and the docs page are not allowed without authentication.
* When we will have a homepage design for non-authenticated users, we will remove this restriction to have
* the full website accessible without authentication.
*/
const regexpUrlsAuth = [/\/docs\/$/g];
export const Auth = ({ children }: PropsWithChildren) => {
const { initAuth, initiated, authenticated, login } = useAuthStore();
const { asPath } = useRouter();
const [pathAllowed, setPathAllowed] = useState<boolean>(
!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)),
);
useEffect(() => {
initAuth();
}, [initAuth]);
useEffect(() => {
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)));
}, [asPath]);
// We force to login except on allowed paths
useEffect(() => {
if (!initiated || authenticated || pathAllowed) {
return;
}
login();
}, [authenticated, pathAllowed, login, initiated]);
if ((!initiated && pathAllowed) || (!authenticated && !pathAllowed)) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
return children;
};
@@ -0,0 +1,27 @@
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/core/auth';
export const ButtonLogin = () => {
const { t } = useTranslation();
const { logout, authenticated, login } = useAuthStore();
if (!authenticated) {
return (
<Button
onClick={() => login(true)}
color="primary-text"
aria-label={t('Login')}
>
{t('Login')}
</Button>
);
}
return (
<Button onClick={logout} color="primary-text" aria-label={t('Logout')}>
{t('Logout')}
</Button>
);
};
@@ -1,7 +1,7 @@
import { Crisp } from 'crisp-sdk-web';
import fetchMock from 'fetch-mock';
import { gotoLogout } from '../utils';
import { useAuthStore } from '../useAuthStore';
jest.mock('crisp-sdk-web', () => ({
...jest.requireActual('crisp-sdk-web'),
@@ -17,7 +17,7 @@ jest.mock('crisp-sdk-web', () => ({
},
}));
describe('utils', () => {
describe('useAuthStore', () => {
afterEach(() => {
jest.clearAllMocks();
fetchMock.restore();
@@ -33,7 +33,7 @@ describe('utils', () => {
writable: true,
});
gotoLogout();
useAuthStore.getState().logout();
expect(Crisp.session.reset).toHaveBeenCalled();
});
@@ -1,6 +1,4 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { APIError, fetchAPI } from '@/api';
import { fetchAPI } from '@/api';
import { User } from './types';
@@ -21,16 +19,3 @@ export const getMe = async (): Promise<User> => {
}
return response.json() as Promise<User>;
};
export const KEY_AUTH = 'auth';
export function useAuthQuery(
queryConfig?: UseQueryOptions<User, APIError, User>,
) {
return useQuery<User, APIError, User>({
queryKey: [KEY_AUTH],
queryFn: getMe,
staleTime: 1000 * 60 * 15, // 15 minutes
...queryConfig,
});
}
@@ -0,0 +1,2 @@
export * from './getMe';
export * from './types';
@@ -0,0 +1 @@
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
@@ -0,0 +1,4 @@
export * from './api/types';
export * from './Auth';
export * from './ButtonLogin';
export * from './useAuthStore';
@@ -0,0 +1,72 @@
import { create } from 'zustand';
import { baseApiUrl } from '@/api';
import { terminateCrispSession } from '@/services';
import { User, getMe } from './api';
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
interface AuthStore {
initiated: boolean;
authenticated: boolean;
initAuth: () => void;
logout: () => void;
login: (directLogin?: boolean) => void;
setAuthUrl: (url: string) => void;
getAuthUrl: () => string | undefined;
userData?: User;
}
const initialState = {
initiated: false,
authenticated: false,
userData: undefined,
};
export const useAuthStore = create<AuthStore>((set, get) => ({
initiated: initialState.initiated,
authenticated: initialState.authenticated,
userData: initialState.userData,
initAuth: () => {
getMe()
.then((data: User) => {
set({ authenticated: true, userData: data });
})
.catch(() => {})
.finally(() => {
set({ initiated: true });
});
},
login: (directLogin?: boolean) => {
if (directLogin) {
get().setAuthUrl(window.location.pathname);
window.location.replace(`${baseApiUrl()}authenticate/`);
return;
}
if (window.location.pathname !== '/') {
get().setAuthUrl(window.location.pathname);
window.location.replace(`/`);
return;
}
},
logout: () => {
terminateCrispSession();
window.location.replace(`${baseApiUrl()}logout/`);
},
// If we try to access a specific page and we are not authenticated
// we store the path in the local storage to redirect to it after login
setAuthUrl() {
if (window.location.pathname !== '/') {
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
}
},
// If a path is stored in the local storage, we return it then remove it
getAuthUrl() {
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) {
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
return path_auth;
}
},
}));
@@ -1,2 +1,3 @@
export * from './AppProvider';
export * from './auth';
export * from './config';
@@ -345,7 +345,6 @@
--c--components--button--disabled--color: white;
--c--components--button--disabled--background--color: #b3cef0;
--c--components--la-gauffre--activated: false;
--c--components--home-proconnect--activated: false;
}
.cunningham-theme--dark {
@@ -591,7 +590,6 @@
);
--c--components--forms-textarea--border-radius: 0;
--c--components--la-gauffre--activated: true;
--c--components--home-proconnect--activated: true;
}
.clr-secondary-text {
@@ -336,7 +336,6 @@ export const tokens = {
disabled: { color: 'white', background: { color: '#b3cef0' } },
},
'la-gauffre': { activated: false },
'home-proconnect': { activated: false },
},
},
dark: {
@@ -582,7 +581,6 @@ export const tokens = {
},
'forms-textarea': { 'border-radius': '0' },
'la-gauffre': { activated: true },
'home-proconnect': { activated: true },
},
},
},
@@ -1,2 +0,0 @@
export * from './useAuthQuery';
export * from './types';
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

@@ -1,47 +0,0 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { useAuth } from '../hooks';
export const Auth = ({ children }: PropsWithChildren) => {
const { isLoading, pathAllowed, isFetchedAfterMount, authenticated } =
useAuth();
const { replace, pathname } = useRouter();
if (isLoading && !isFetchedAfterMount) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
/**
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
*/
if (!authenticated && !pathAllowed) {
void replace('/login');
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
/**
* If the user is authenticated and the path is the login page, we redirect to the home page.
*/
if (pathname === '/login' && authenticated) {
void replace('/');
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
return children;
};

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