Compare commits

...

32 Commits

Author SHA1 Message Date
Manuel Raynaud fed3ad6a81 fixup! 📝(doc) add documentation to install using compose 2025-01-28 16:31:49 +01:00
Manuel Raynaud 350643a4c8 📝(doc) add documentation to install using compose
Write the documentation related to the docker compose installation.
2025-01-28 12:35:30 +01:00
Manuel Raynaud 6f62d8ec2a (compose) a config to deploy docs in production using compose
To ease the deployment of the application on infrastructure not running
k8s we created a compose file for production.
2025-01-28 12:35:30 +01:00
Manuel Raynaud 24328b5d6b ♻️(compose) remove usage of dockerize
Compose is able to manage service status to be healthy in its
dependecies. Configuring a healthcheck on services then defining a
condition on the depends_on settings allow to remove dockerize
2025-01-28 10:41:20 +01:00
Manuel Raynaud 9179fdb2fa 🔧(compose) rename legacy docker-compose.yml in compose.yaml
The compose configuration preferred filename is now compose.yaml
docker-compose.yml is the legacy one used by docker-compose v1
2025-01-28 10:41:18 +01:00
Manuel Raynaud 0d7d42254b (helm) add a job allowing to run arbitrary management command
For a specific deployment we may need to run a specific management
command, like the one added previously updating all files content-type.
A template is added responsible to manage this case. The job will be
created only if the backend.job.command is set.
2025-01-28 10:33:30 +01:00
Anthony LC 67dc7feb98 🚑️(backend) command to update attachment content-type
The uploaded files in the system are missing
the content-type.
We add a command to update the content-type of
the existing uploaded files.
This command will run one time when we will deploy
to the environments.
2025-01-28 10:33:30 +01:00
Anthony LC 5b4b100e90 🏷️(backend) add content-type to uploaded files
All the uploaded files had the content-type set
to `application/octet-stream`. It create issues
when the file is downloaded from the frontend
because the browser doesn't know how to handle
the file.
We now determine the content-type of the file
and set it to the file object.
2025-01-28 10:33:30 +01:00
Anthony LC b8be010389 🚚(helm) add posthog proxy
To contourn ads blocker, we add a proxy to the
posthog service. This way, we can access the
service from the same domain as the frontend.
2025-01-28 10:05:37 +01:00
Anthony LC 97cfa2c1ad (frontend) integrate posthog analytics
We integrate posthog, it will help us to track
user behavior and improve the product.
We get the configuration from the backend config
endpoint.
2025-01-28 10:05:37 +01:00
Anthony LC c018c6fcf5 🔧(backend) add posthog configuration
We add the posthog configuration to the project.
We will expose the posthog configuration to the
frontend.
2025-01-28 10:05:37 +01:00
Nathan Panchout 70048328d1 (frontend) update document sharing tests
- Added Share button interactions in various document visibility
scenarios
- Updated test assertions for share and copy link functionality
- Improved test coverage for document sharing features
2025-01-28 08:59:28 +01:00
Nathan Panchout 55ddfe9181 💄(frontend) refactor document sharing and grid components
Improvements:
- Added disabled state for dropdown menus in share settings
- Updated document grid layout and responsiveness
- Simplified sharing and access count logic
- Improved tooltips and visibility of shared documents
- Created a new responsive doc grid hook
2025-01-28 08:59:28 +01:00
renovate[bot] ee41d156c7 ⬆️(dependencies) update django to v5.1.5 [SECURITY] 2025-01-27 10:06:16 +01:00
Manuel Raynaud 5be2bc7360 ♻️(actions) create a reusable workflow to install front dependencies
In more than one workflow we need to install frontend dependencies and
this 3 workflows we are copy/pasting the same code. We want to refactor
this by creating a reusable workflow
https://docs.github.com/en/actions/sharing-automations/reusing-workflows
2025-01-24 12:22:48 +01:00
Manuel Raynaud e46ba4f506 🌐(back) update source translations
In order to have a repo correctly confiogured to managed translation
with crowdin, source translations for the backend app are updated
2025-01-24 12:22:48 +01:00
Manuel Raynaud 7c8b969fa9 🔥(back) remove compiled translation file
The compiles translation file should not be track with git.
2025-01-24 12:22:48 +01:00
Manuel Raynaud 95515fd460 🌐(action) create a workflow to download translation
A new workflow is added responsible todownload new translated strings
and then create a pull request to update the code.
2025-01-24 12:22:48 +01:00
Manuel Raynaud ce6cfc22ef 🌐(action) upload sources translation on crowdin
Crowdin has released its own github action to automatize translation
workflow. We want to use to upload sources when a PR is merged.
2025-01-24 12:22:48 +01:00
virgile-dev 4b3b441fc3 📝(project) update readme following upgrade to v2
Remove old mentions to "impress" following the repository renaming.
Improve and update descriptions to better reflect the status of the
project after release version 2.
2025-01-21 23:39:22 +01:00
Anthony LC 9194bf5a90 🔖(patch) release 2.0.1
Fixed:
🐛(frontend) title copy break app
2025-01-17 11:58:55 +01:00
Anthony LC dc63a5839e ⬇️(frontend) downgraded blocknote to 0.21.0
The last version of Blocknote (0.22.0) has a bug,
when we copy paste a title, the app sometimes crashes.
Better to downgrade to 0.21.0 until the bug is fixed.
2025-01-17 11:33:47 +01:00
Anthony LC d406846986 🎨(frontend) format css blocknote editor
We use the "css" function of style components
to format correctly the blocknote editor css.
2025-01-17 11:33:47 +01:00
Nathan Panchout e85b07021e 🐛(frontend) fix collaboration cursor
- The collaboration slider is not fully shown when a user is at the very
top of the document
2025-01-17 11:02:41 +01:00
Nathan Panchout 282200ac3d 🐛(frontend) hide the sharing method when you don't have the rights
- Added a new hook `useCopyDocLink` to handle copying document links to
the clipboard with success/error notifications.
- Updated the `DocToolBox`, `DocsGridActions`, and `DocShareModal`
components to utilize the new copy link feature.
- Enhanced tests to verify the functionality of the copy link button in
various scenarios.
- Adjusted visibility checks for sharing options based on user access
rights.
2025-01-17 11:02:41 +01:00
Anthony LC de8dea20d5 🔖(major) release 2.0.0
Added:
- 🔧(backend) add option to configure list of
essential OIDC claims
- 🔧(helm) add option to disable default tls
setting by @dominikkaminski
- 💄(frontend) Add left panel
- 💄(frontend) add filtering to left panel
- (frontend) new share modal ui
- (frontend) add favorite feature

Changed:
- 🏗️(yjs-server) organize yjs server
- ♻️(frontend) better separation collaboration
process
- 💄(frontend) updating the header and leftpanel
for responsive
- 💄(frontend) update DocsGrid component
- 💄(frontend) update DocsGridOptions component
- 💄(frontend) update DocHeader ui
- 💄(frontend) update doc versioning ui
- 💄(frontend) update doc summary u

Fixed:
- 🐛(backend) fix create document via s2s
if sub unknown but email found
- 🐛(frontend) hide search and create doc
button if not authenticated
- 🐛(backend) race condition creation issue
2025-01-15 12:46:00 +01:00
Anthony LC 342fc2ab59 ✏️(backend) fix read_only_fields is_favorite
is_favorite has a typo error.
This commit fixes it.
2025-01-15 12:13:40 +01:00
Anthony LC b8132ef393 🐛(backend) creation race condition
3 requests we able to create a document:
- POST document request
- GET collaboration-auth
- GET media-auth

If the 2 last were faster than the first, a
document was created without the necessary
informations.
2025-01-15 12:13:40 +01:00
Nathan Panchout 2ede746d8a (frontend) hide search and create doc button if not logged
- Added visibility checks for 'search' and 'New doc' buttons in the
document visibility tests.
- Updated LeftPanelHeader to conditionally render 'search' and 'New doc'
buttons based on user authentication status, improving user experience
and access control.
2025-01-15 12:00:40 +01:00
Anthony LC 5bd0764bdd 🌐(frontend) add last translations
Add the missing translations FR / DE.
2025-01-14 17:38:19 +01:00
Samuel Paccoud - DINUM 610948cd16 🐛(backend) fix create document for user when sub does not match
When creating a document on behalf of a user via the server-to-server
API, a special edge case was broken that should should never happen
but happens in our OIDC federation because one of the provider modifies
the users "sub" each time they login.

We end-up with existing users for who the email matches but not the sub.
They were not correctly handled.

I made a few additional fixes and improvements to the endpoint.
2025-01-14 16:18:14 +01:00
Samuel Paccoud - DINUM 96bb99d6ec 🐛(compose) fix "port already taken" errors when starting docker compose
We have changed the project's name from "impress" to "docs" but haven't
replaced all occurrences of impress in the project because we want to be
careful of the consequences on deployments.

The name of the docker compose project was different for the "make pylint"
target. This was causing the bug error on ports. Let's rename it without
waiting.
2025-01-14 16:18:14 +01:00
101 changed files with 3333 additions and 817 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Download translations from Crowdin
on:
workflow_dispatch:
push:
branches:
- 'release/**'
jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
synchronize-with-crowdin:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create empty source files
run: |
touch src/backend/locale/django.pot
mkdir -p src/frontend/packages/i18n/locales/impress/
touch src/frontend/packages/i18n/locales/impress/translations-crowdin.json
# crowdin workflow
- name: crowdin action
uses: crowdin/github-action@v2
with:
config: crowdin/config.yml
upload_sources: false
upload_translations: false
download_translations: true
create_pull_request: false
push_translations: false
push_sources: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
# Visit https://crowdin.com/settings#api-key to create this token
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
CROWDIN_BASE_PATH: "../src/"
# frontend i18n
- name: Restore the frontend cache
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
fail-on-cache-miss: true
- name: generate translations files
working-directory: src/frontend
run: yarn i18n:deploy
# Create a new PR
- name: Create a new Pull Request with new translated strings
uses: peter-evans/create-pull-request@v7
with:
commit-message: |
🌐(i18n) update translated strings
Update translated files with new translations
title: 🌐(i18n) update translated strings
body: |
## Purpose
update translated strings
## Proposal
- [x] update translated strings
branch: i18n/update-translations
labels: i18n
+67
View File
@@ -0,0 +1,67 @@
name: Update crowdin sources
on:
workflow_dispatch:
push:
branches:
- main
jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
synchronize-with-crowdin:
needs: install-front
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Backend i18n
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.12.6"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
run: pip install --user .
working-directory: src/backend
- name: Install gettext
run: |
sudo apt-get update
sudo apt-get install -y gettext pandoc
- name: generate pot files
working-directory: src/backend
run: |
DJANGO_CONFIGURATION=Build python manage.py makemessages -a --keep-pot
# frontend i18n
- name: Restore the frontend cache
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
fail-on-cache-miss: true
- name: generate source translation file
working-directory: src/frontend
run: yarn i18n:extract
# crowdin workflow
- name: crowdin action
uses: crowdin/github-action@v2
with:
config: crowdin/config.yml
upload_sources: true
upload_translations: false
download_translations: false
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
# Visit https://crowdin.com/settings#api-key to create this token
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
CROWDIN_BASE_PATH: "../src/"
@@ -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') }}
+20 -38
View File
@@ -9,39 +9,15 @@ on:
- "*"
jobs:
install-front:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- 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: 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') }}
uses: ./.github/workflows/front-dependencies-installation.yml
with:
node_version: '20.x'
test-front:
runs-on: ubuntu-latest
needs: install-front
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -53,10 +29,10 @@ jobs:
- 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') }}
fail-on-cache-miss: true
- name: Test App
run: cd src/frontend/ && yarn test
@@ -68,29 +44,39 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- 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') }}
fail-on-cache-miss: true
- name: Check linting
run: cd src/frontend/ && yarn lint
test-e2e-chromium:
runs-on: ubuntu-latest
needs: install-front
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- 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') }}
fail-on-cache-miss: true
- name: Set e2e env variables
run: cat env.d/development/common.e2e.dist >> env.d/development/common.dist
@@ -141,12 +127,8 @@ jobs:
- name: Checkout repository
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: Install frontend dependencies
uses: ./.github/workflows/front-dependencies-installation.yml
- name: Set e2e env variables
run: cat env.d/development/common.e2e.dist >> env.d/development/common.dist
+3 -2
View File
@@ -206,10 +206,11 @@ jobs:
- name: Install development dependencies
run: pip install --user .[dev]
- name: Install gettext (required to compile messages)
- name: Install gettext (required to compile messages) and MIME support
run: |
sudo apt-get update
sudo apt-get install -y gettext pandoc
sudo apt-get install -y gettext pandoc shared-mime-info
sudo wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
- name: Generate a MO file from strings extracted from the project
run: python manage.py compilemessages
+2
View File
@@ -30,6 +30,7 @@ MANIFEST
.next/
# Translations # Translations
*.mo
*.pot
# Environments
@@ -40,6 +41,7 @@ ENV/
env.bak/
venv.bak/
env.d/development/*
env.d/production/*
!env.d/development/*.dist
env.d/terraform
+30 -2
View File
@@ -11,6 +11,26 @@ and this project adheres to
## Added
- github actions to managed Crowdin workflow
- 📈Integrate Posthog #540
- 🏷️(backend) add content-type to uploaded files #552
## Changed
- 💄(frontend) add abilities on doc row #581
## [2.0.1] - 2025-01-17
## Fixed
-🐛(frontend) share modal is shown when you don't have the abilities #557
-🐛(frontend) title copy break app #564
## [2.0.0] - 2025-01-13
## Added
- 🔧(backend) add option to configure list of essential OIDC claims #525 & #531
- 🔧(helm) add option to disable default tls setting by @dominikkaminski #519
- 💄(frontend) Add left panel #420
@@ -25,10 +45,16 @@ and this project adheres to
- 💄(frontend) updating the header and leftpanel for responsive #421
- 💄(frontend) update DocsGrid component #431
- 💄(frontend) update DocsGridOptions component #432
- 💄(frontend) update DocHeader ui #446
- 💄(frontend) update DocHeader ui #448
- 💄(frontend) update doc versioning ui #463
- 💄(frontend) update doc summary ui #473
- 📝(doc) update readme.md to match V2 changes #558
## Fixed
- 🐛(backend) fix create document via s2s if sub unknown but email found #543
- 🐛(frontend) hide search and create doc button if not authenticated #555
- 🐛(backend) race condition creation issue #556
## [1.10.0] - 2024-12-17
@@ -346,7 +372,9 @@ and this project adheres to
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.10.0...main
[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
[v1.9.0]: https://github.com/numerique-gouv/impress/releases/v1.9.0
[v1.8.2]: https://github.com/numerique-gouv/impress/releases/v1.8.2
+8 -1
View File
@@ -51,7 +51,7 @@ COPY ./src/backend /app/
WORKDIR /app
# collectstatic
RUN DJANGO_CONFIGURATION=Build DJANGO_JWT_PRIVATE_SIGNING_KEY=Dummy \
RUN DJANGO_CONFIGURATION=Build \
python manage.py collectstatic --noinput
# Replace duplicated file by a symlink to decrease the overall size of the
@@ -76,6 +76,8 @@ RUN apk add \
pango \
shared-mime-info
RUN wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
# Copy entrypoint
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
@@ -92,6 +94,11 @@ COPY ./src/backend /app/
WORKDIR /app
# Generate compiled translation messages
RUN DJANGO_CONFIGURATION=Build \
python manage.py compilemessages
# We wrap commands run in this container by the following entrypoint that
# creates a user on-the-fly with the container user ID (see USER) and root group
# ID.
+41 -6
View File
@@ -38,13 +38,13 @@ DB_PORT = 5432
DOCKER_UID = $(shell id -u)
DOCKER_GID = $(shell id -g)
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
COMPOSE = DOCKER_USER=$(DOCKER_USER) ./bin/compose
COMPOSE_PRODUCTION = DOCKER_USER=$(DOCKER_USER) COMPOSE_FILE=compose.production.yaml ./bin/compose
COMPOSE_EXEC = $(COMPOSE) exec
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
@@ -65,6 +65,19 @@ data/media:
data/static:
@mkdir -p data/static
# -- production volumes
data/production/media:
@mkdir -p data/production/media
data/production/certs:
@mkdir -p data/production/certs
data/production/databases/backend:
@mkdir -p data/production/databases/backend
data/production/databases/keycloak:
@mkdir -p data/production/databases/keycloak
# -- Project
create-env-files: ## Copy the dist env files to env files
@@ -89,6 +102,27 @@ bootstrap: \
mails-build
.PHONY: bootstrap
bootstrap-production: ## Prepare project to run in production mode using docker compose
bootstrap-production: \
env.d/production \
data/production/media \
data/production/certs \
data/production/databases/backend \
data/production/databases/keycloak
bootstrap-production:
@echo 'Environment files created in env.d/production'
@echo 'Edit them to set good value for your production environment'
.PHONY: bootstrap-production
run-production: ## Run compose project in production mode
@$(COMPOSE_PRODUCTION) up -d ingress
.PHONY: run-production
stop-production: ## Stop compose project in production mode
@$(COMPOSE_PRODUCTION) stop
.PHONY: stop-production
# -- Docker/compose
build: cache ?= --no-cache
build: ## build the project containers
@@ -124,8 +158,6 @@ 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
@echo "Wait for postgresql to be up..."
@$(WAIT_DB)
.PHONY: run
run-with-frontend: ## Start all the containers needed (backend to frontend)
@@ -188,14 +220,12 @@ 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
@@ -229,6 +259,8 @@ resetdb: ## flush database and create a superuser "admin"
@${MAKE} superuser
.PHONY: resetdb
# -- Environment variable files
env.d/development/common:
cp -n env.d/development/common.dist env.d/development/common
@@ -238,6 +270,9 @@ env.d/development/postgresql:
env.d/development/kc_postgresql:
cp -n env.d/development/kc_postgresql.dist env.d/development/kc_postgresql
env.d/production:
cp -rnf env.d/production.dist env.d/production
# -- Internationalization
env.d/development/crowdin:
+109 -51
View File
@@ -1,113 +1,171 @@
# Impress
<p align="center">
<a href="https://github.com/suitenumerique/docs">
<img alt="Docs" src="/docs/assets/logo-docs.png" width="300" />
</a>
</p>
Impress is a web application for real-time collaborative text editing with user and role based access rights.
Features include :
- User authentication through OIDC
- BlocNote.js text editing experience (markdown support, dynamic conversion, block structure, slash commands for block creation)
- Document export to pdf and docx from predefined templates
- Granular document permissions
- Public link sharing
- Offline mode
<p align="center">
Welcome to Docs! The open source document editor where your notes can become knowledge through live collaboration
</p>
Impress is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/) and [BlocNote.js](https://www.blocknotejs.org/)
<p align="center">
<a href="https://matrix.to/#/#docs-official:matrix.org">
Chat on Matrix
</a> - <a href="/docs/">
Documentation
</a> - <a href="#getting-started">
Getting started
</a>
</p>
## Getting started
<img src="/docs/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
### Prerequisite
## Why use Docs ❓
Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing.
Make sure you have a recent version of Docker and [Docker
Compose](https://docs.docker.com/compose/install) installed on your laptop:
### Write
* 😌 Simple collaborative editing without the formatting complexity of markdown
* 🔌 Offline? No problem, keep writing, your edits will get synced when back online
* 💅 Create clean documents with limited but beautiful formatting options and focus on content
* 🧱 Built for productivity (markdown support, many block types, slash commands, markdown support, keyboard shortcuts) (page in french sorry 😅).
* ✨ Save time thanks to our AI actions (generate, sum up, correct, translate)
```bash
### Collaborate
* 🤝 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 transform your team's collaborative work into organized knowledge `ETA 02/2025`
### Self-host
* 🚀 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
**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 version 27.4.1, build b9d17ea
$ docker compose -v
docker compose version 1.27.4, build 40524192
$ docker compose version
Docker Compose version v2.32.1
```
> ⚠️ 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
> ⚠️ 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:
```bash
```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-releated or migration-releated 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 🎉
You can access to the project by going to http://localhost:3000.
You can access to the project by going to <http://localhost:3000>.
You will be prompted to log in, the default credentials are:
```bash
```shellscript
username: impress
password: impress
```
📝 Note that if you need to run them afterwards, you can use the eponym Make rule:
```bash
```shellscript
$ make run-with-frontend
```
---
⚠️ For the frontend developper, it is often better to run the frontend in development mode locally.
⚠️ For the frontend developper, it is often better to run the frontend in development mode locally.
To do so, install the frontend dependencies with the following command:
```bash
```shellscript
$ make frontend-install
```
And run the frontend locally in development mode with the following command:
```bash
```shellscript
$ make run-frontend-development
```
To start all the services, except the frontend container, you can use the following command:
```bash
```shellscript
$ make run
```
---
### Adding content
**Adding content**
You can create a basic demo site by running:
$ make demo
```shellscript
$ make demo
```
Finally, you can check all available Make rules using:
```bash
```shellscript
$ make help
```
### Django admin
**Django admin**
You can access the Django admin site at
[http://localhost:8071/admin](http://localhost:8071/admin).
<http://localhost:8071/admin>.
You first need to create a superuser account:
```bash
```shellscript
$ make superuser
```
## Contributing
## 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).
This project is intended to be community-driven, so please, do not hesitate to
get in touch if you have any question related to our implementation or design
decisions.
## Roadmap
Want to know where the project is headed? [🗺️ Checkout our roadmap](https://github.com/orgs/numerique-gouv/projects/13/views/11)
## License
## Licence 📝
This work is released under the MIT License (see [LICENSE](https://github.com/suitenumerique/docs/blob/main/LICENSE)).
This work is released under the MIT License (see [LICENSE](./LICENSE)).
While Docs is 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 if you have any question related to our implementation or design decisions.
If you intend to make pull requests see CONTRIBUTING for guidelines.
Directory structure:
```markdown
docs
├── bin - executable scripts or binaries that are used for various tasks, such as setup scripts, utility scripts, or custom commands.
├── crowdin - for crowdin translations, a tool or service that helps manage translations for the project.
├── docker - Dockerfiles and related configuration files used to build Docker images for the project. These images can be used for development, testing, or production environments.
├── docs - documentation for the project, including user guides, API documentation, and other helpful resources.
├── env.d/development - environment-specific configuration files for the development environment. These files might include environment variables, configuration settings, or other setup files needed for development.
├── gitlint - configuration files for `gitlint`, a tool that enforces commit message guidelines to ensure consistency and quality in commit messages.
├── playground - experimental or temporary code, where developers can test new features or ideas without affecting the main codebase.
└── src - main source code directory, containing the core application code, libraries, and modules of the project.
```
## Credits ❤️
### Stack
Impress is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [MinIO](https://min.io/) and [BlocNote.js](https://www.blocknotejs.org/)
### States ❤️ open source
Docs is the result of a joint effort lead by the French 🇫🇷🥖 ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German 🇩🇪🥨 government ([ZenDiS](https://zendis.de/)). We are always looking for new public partners feel free to reach out if you are interested in using or contributing to docs.
+4 -5
View File
@@ -6,9 +6,9 @@ REPO_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
UNSET_USER=0
TERRAFORM_DIRECTORY="./env.d/terraform"
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
COMPOSE_PROJECT="impress"
if [ -z ${COMPOSE_FILE+x} ]; then
COMPOSE_FILE="${REPO_DIR}/compose.yaml"
fi
# _set_user: set (or unset) default user id used to run docker commands
#
@@ -40,9 +40,8 @@ function _set_user() {
# ARGS : docker compose command arguments
function _docker_compose() {
echo "🐳(compose) project: '${COMPOSE_PROJECT}' file: '${COMPOSE_FILE}'"
echo "🐳(compose) project, file: '${COMPOSE_FILE}'"
docker compose \
-p "${COMPOSE_PROJECT}" \
-f "${COMPOSE_FILE}" \
--project-directory "${REPO_DIR}" \
"$@"
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
set -o errexit
# The script is pretty simple. It downloads the latest cacert.pem file from the certifi package and appends the root certificate from mkcert to it. Then it copies the updated cacert.pem file to the container.
# The script is executed with the following command:
# $ bin/update_app_cacert.sh docs-production-backend-1
CONTAINER_NAME=${1:-"docs-production-backend-1"}
echo "updating cacert.pem for certifi package in ${CONTAINER_NAME}"
curl --create-dirs https://raw.githubusercontent.com/certifi/python-certifi/refs/heads/master/certifi/cacert.pem -o /tmp/certifi/cacert.pem
cat "$(mkcert -CAROOT)/rootCA.pem" >> /tmp/certifi/cacert.pem
docker cp /tmp/certifi/cacert.pem ${CONTAINER_NAME}:/usr/local/lib/python3.12/site-packages/certifi/cacert.pem
echo "end patching cacert.pem in ${CONTAINER_NAME}"
+167
View File
@@ -0,0 +1,167 @@
name: docs-production
services:
postgresql:
image: postgres:16
healthcheck:
test: ["CMD", "pg_isready", "-q", "-U", "docs", "-d", "docs"]
interval: 1s
timeout: 2s
retries: 300
env_file:
- env.d/production/postgresql
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ./data/production/databases/backend:/var/lib/postgresql/data/pgdata
redis:
image: redis:5
backend-migration:
image: lasuite/impress-backend:latest
user: ${DOCKER_USER:-1000}
command: ["python", "manage.py", "migrate", "--noinput"]
environment:
- DJANGO_CONFIGURATION=Production
env_file:
- env.d/production/backend
- env.d/production/postgresql
- env.d/production/yprovider
depends_on:
postgresql:
condition: service_healthy
restart: true
redis:
condition: service_started
minio:
condition: service_started
backend:
image: lasuite/impress-backend:latest
user: ${DOCKER_USER:-1000}
restart: always
environment:
- DJANGO_CONFIGURATION=Production
env_file:
- env.d/production/backend
- env.d/production/postgresql
- env.d/production/yprovider
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 15s
timeout: 30s
retries: 20
start_period: 10s
depends_on:
postgresql:
condition: service_healthy
restart: true
backend-migration:
condition: service_completed_successfully
redis:
condition: service_started
minio:
condition: service_started
minio-bootstrap:
condition: service_completed_successfully
celery:
user: ${DOCKER_USER:-1000}
image: lasuite/impress-backend:latest
command: ["celery", "-A", "impress.celery_app", "worker", "-l", "INFO"]
environment:
- DJANGO_CONFIGURATION=Production
env_file:
- env.d/production/backend
- env.d/production/postgresql
- env.d/production/yprovider
depends_on:
- backend
frontend:
image: lasuite/impress-frontend:latest
user: ${DOCKER_USER:-1000}
y-provider:
image: lasuite/impress-y-provider:latest
user: ${DOCKER_USER:-1000}
env_file:
- env.d/production/yprovider
kc_postgresql:
image: postgres:16
healthcheck:
test: ["CMD", "pg_isready", "-q", "-U", "keycloak", "-d", "keycloak"]
interval: 1s
timeout: 2s
retries: 300
env_file:
- env.d/production/kc_postgresql
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ./data/production/databases/keycloak:/var/lib/postgresql/data/pgdata
keycloak:
image: quay.io/keycloak/keycloak:26.1.0
command: ["start"]
env_file:
- env.d/production/keycloak
- env.d/production/kc_postgresql
ports:
- "8443:8443"
volumes:
- ${DOCS_PROD_KEYCLOAK_CERT_FOLDER:-./data/production/certs}:/etc/ssl/certs:ro
depends_on:
kc_postgresql:
condition: service_healthy
restart: true
minio-bootstrap:
image: minio/mc
env_file:
- env.d/production/minio
depends_on:
minio:
condition: service_healthy
restart: true
entrypoint: >
sh -c "
/usr/bin/mc alias set docs http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD} && \
/usr/bin/mc mb --ignore-existing docs/docs-media-storage && \
/usr/bin/mc version enable docs/docs-media-storage && \
exit 0;"
minio:
user: ${DOCKER_USER:-1000}
image: minio/minio
env_file:
- env.d/production/minio
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 20s
retries: 300
entrypoint: ""
command: minio server /data
volumes:
- ./data/production/media:/data
ingress:
image: nginx:1.27
ports:
- "${DOCS_PROD_NGING_PORT:-443}:8083"
volumes:
- ./docker/files/production/etc/nginx/conf.d:/etc/nginx/conf.d:ro
- ${DOCS_PROD_NGINX_CERT_FOLDER:-./data/production/certs}:/etc/nginx/ssl:ro
depends_on:
frontend:
condition: service_started
y-provider:
condition: service_started
keycloak:
condition: service_started
backend:
condition: service_healthy
restart: true
+40 -13
View File
@@ -1,6 +1,13 @@
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:
@@ -23,6 +30,11 @@ 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:
@@ -31,7 +43,9 @@ services:
createbuckets:
image: minio/mc
depends_on:
- minio
minio:
condition: service_healthy
restart: true
entrypoint: >
sh -c "
/usr/bin/mc alias set impress http://minio:9000 impress password && \
@@ -59,10 +73,15 @@ services:
- ./src/backend:/app
- ./data/static:/data/static
depends_on:
- postgresql
- mailcatcher
- redis
- createbuckets
postgresql:
condition: service_healthy
restart: true
mailcatcher:
condition: service_started
redis:
condition: service_started
createbuckets:
condition: service_started
celery-dev:
user: ${DOCKER_USER:-1000}
@@ -93,9 +112,13 @@ services:
- env.d/development/common
- env.d/development/postgresql
depends_on:
- postgresql
- redis
- minio
postgresql:
condition: service_healthy
restart: true
redis:
condition: service_started
minio:
condition: service_started
celery:
user: ${DOCKER_USER:-1000}
@@ -135,9 +158,6 @@ services:
ports:
- "3000:3000"
dockerize:
image: jwilder/dockerize
crowdin:
image: crowdin/cli:3.16.0
volumes:
@@ -151,7 +171,7 @@ services:
image: node:18
user: "${DOCKER_USER:-1000}"
environment:
HOME: /tmp
HOME: /tmp
volumes:
- ".:/app"
@@ -169,6 +189,11 @@ services:
kc_postgresql:
image: postgres:14.3
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 1s
timeout: 2s
retries: 300
ports:
- "5433:5432"
env_file:
@@ -200,4 +225,6 @@ services:
ports:
- "8080:8080"
depends_on:
- kc_postgresql
kc_postgresql:
condition: service_healthy
restart: true
+6 -6
View File
@@ -1,7 +1,7 @@
#
# Your crowdin's credentials
#
api_token_env: CROWDIN_API_TOKEN
api_token_env: CROWDIN_PERSONAL_TOKEN
project_id_env: CROWDIN_PROJECT_ID
base_path_env: CROWDIN_BASE_PATH
@@ -15,11 +15,11 @@ preserve_hierarchy: true
# Files configuration
#
files: [
{
source : "/backend/locale/django.pot",
dest: "/backend-impress.pot",
translation : "/backend/locale/%locale_with_underscore%/LC_MESSAGES/django.po"
},
{
source : "/backend/locale/django.pot",
dest: "/backend-impress.pot",
translation : "/backend/locale/%locale_with_underscore%/LC_MESSAGES/django.po"
},
{
source: "/frontend/packages/i18n/locales/impress/translations-crowdin.json",
dest: "/frontend-impress.json",
@@ -0,0 +1,132 @@
upstream docs_backend {
server backend:8000 fail_timeout=0;
}
upstream docs_frontend {
server frontend:8080 fail_timeout=0;
}
server {
listen 8083 ssl;
server_name localhost;
# Disables server version feedback on pages and in headers
server_tokens off;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location @proxy_to_docs_backend {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://docs_backend;
}
location @proxy_to_docs_frontend {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://docs_frontend;
}
location / {
try_files $uri @proxy_to_docs_frontend;
}
location /api {
try_files $uri @proxy_to_docs_backend;
}
location /admin {
try_files $uri @proxy_to_docs_backend;
}
# Proxy auth for collaboration server
location /collaboration/ws/ {
# Collaboration Auth request configuration
auth_request /collaboration-auth;
proxy_set_header X-Forwarded-Proto https;
auth_request_set $authHeader $upstream_http_authorization;
auth_request_set $canEdit $upstream_http_x_can_edit;
auth_request_set $userId $upstream_http_x_user_id;
# Pass specific headers from the auth response
proxy_set_header Authorization $authHeader;
proxy_set_header X-Can-Edit $canEdit;
proxy_set_header X-User-Id $userId;
# Ensure WebSocket upgrade
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# Collaboration server
proxy_pass http://y-provider:4444;
# Set appropriate timeout for WebSocket
proxy_read_timeout 86400;
proxy_send_timeout 86400;
# Preserve original host and additional headers
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Origin $http_origin;
proxy_set_header Host $host;
}
location /collaboration-auth {
proxy_pass http://docs_backend/api/v1.0/documents/collaboration-auth/;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Original-URL $request_uri;
# Prevent the body from being passed
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
}
location /collaboration/api/ {
# Collaboration server
proxy_pass http://y-provider:4444;
proxy_set_header Host $host;
}
# Proxy auth for media
location /media/ {
# Auth request configuration
auth_request /media-auth;
auth_request_set $authHeader $upstream_http_authorization;
auth_request_set $authDate $upstream_http_x_amz_date;
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
# Pass specific headers from the auth response
proxy_set_header Authorization $authHeader;
proxy_set_header X-Amz-Date $authDate;
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
# Get resource from Minio
proxy_pass http://minio:9000/docs-media-storage/;
proxy_set_header Host minio:9000;
}
location /media-auth {
proxy_pass http://docs_backend/api/v1.0/documents/media-auth/;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Original-URL $request_uri;
# Prevent the body from being passed
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

+7
View File
@@ -40,6 +40,7 @@ backend:
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE: https://impress.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://impress.127.0.0.1.nip.io
POSTHOG_KEY: "{'id': 'posthog_key', 'host': 'https://product.impress.127.0.0.1.nip.io'}"
DB_HOST: postgresql
DB_NAME: impress
DB_USER: dinum
@@ -121,6 +122,12 @@ yProvider:
COLLABORATION_SERVER_SECRET: my-secret
Y_PROVIDER_API_KEY: my-secret
posthog:
ingress:
enabled: false
ingressAssets:
enabled: false
ingress:
enabled: true
host: impress.127.0.0.1.nip.io
+66
View File
@@ -0,0 +1,66 @@
# Installation with docker compose
We provide a configuration for running Docs in production using docker compose. This configuration is experimental, the official way to deploy Docs in production is to use [k8s](docs/installation/k8s.md)
## Requirements
- A modern version of Docker and its Compose plugin.
- SSL certificates for Docs domain and Keycloak.
- Two domain name. One for the Docs application and an other one for Keycloak. Both can be a subdomain of a common domain. (example: docs.domain.tld and keycloak.domain.tld)
## Installation
- Clone this repository: `git clone https://github.com/suitenumerique/docs.git`
- Then in the clone directory you can run the following command: `make bootsrap-production`
## Configure your ssl certificates
You have to provide the ssl certificates. The easiest way is to use [certbot](https://certbot.eff.org/), generate the certificates with it (both for Docs and Keycloak) and then mount them in ingress and keycloak containers. Two environment variables can be used for that:
- `DOCS_PROD_NGINX_CERT_FOLDER` path to the folder containing the certificates for Docs. This folder will be mounted in `/etc/nginx/ssl` in the container. You have to adapt the certificates name in the file `docker/files/production/etc/nginx/conf.d/default.conf` accordingly with the certificates name you have (see `ssl_certificate` and `ssl_certificate_key` directives).
- `DOCS_PROD_KEYCLOAK_CERT_FOLDER` path to the folder containing the certificates for Keycloak. This folder will be mounted in `/etc/ssl/certs` in the container. You have to adapt the certificates name in the configuration file in `env.d/production/keycloak` to add the correct path for environment variables `KC_HTTPS_CERTIFICATE_FILE` and `KC_HTTPS_CERTIFICATE_KEY_FILE`.
### Configuration
All the configuration files are in the directory `env.d/production`. You have to edit all the files to complete them. For the OIDC information you will have them once Keycloak will be running and you have configured your own realm on it.
#### env.d/production/minio
All the settings related to Minio. You have to set a username and a password to manage the minio cluster. You will need them later in the `env.d/production/backend` file.
#### env.d/production/postgresql
All the settings related to the Postgresql database used by the Django application.
#### env.d/production/yprovider
All the settings related to the collaboration server. All the secret and api key must be generated.
#### env.d/production/kc_postgresql
All the settings related to the Postgresql database used by keycloak.
#### env.d/production/keycloak
All the settings related to the Keycloak application.
#### env.d/production/backend
All the settings related to the Django application. Only the settings you don't have for now are all the one related to OIDC. You will have them once the compose started and you can access to Keycloak.
## Run the compose configuration
The compose configuration can be run with the following command: `make run-production`. The first start can be a little bit long, lots of things are created. Once started you can check that everything is running with the following command: `COMPOSE_FILE=compose.production.yaml ./bin/compose ps`
## Configure keycloak
You have to create a new realm in your Keycloak and once created you have to create a new OIDC client in it. You will use this client to configure the OIDC part in `env.d/production/backend`. This is the last missing part to complete the Django application configuration.
Once the client information set in `env.d/production/backend` you have to start the containers again by running the commande `make run-production`. The command will recreate the containers with the good configuration.
### Helpers
there is a helper script to control the `docker compose` command. You can export the variable `COMPOSE_FILE` with the compose filename (`export COMPOSE_FILE=compose.production.yaml`). After you can run `./bin/compose` to run the docker compose command line.
Makefile commands available:
- `make bootstrap-production`: create the configuration files in `env.d/production`, create the directories : `data/production`. Both directories must be backup, if you loose them you loose all the data related to the application.
- `make run-production`: up the ingress containers. Will start all the containers needed in cascade.
- `make stop-production`: stop all the containers.
+1 -1
View File
@@ -1,3 +1,3 @@
CROWDIN_API_TOKEN=Your-Api-Token
CROWDIN_PERSONAL_TOKEN=Your-Personal-Token
CROWDIN_PROJECT_ID=Your-Project-Id
CROWDIN_BASE_PATH=/app/src
+58
View File
@@ -0,0 +1,58 @@
## Django
DJANGO_ALLOWED_HOSTS=impress.127.0.0.1.nip.io,keycloack.127.0.0.1.nip.io
DJANGO_SECRET_KEY=ThisIsAnExampleKeyForDevPurposeOnly
DJANGO_SETTINGS_MODULE=impress.settings
DJANGO_SUPERUSER_PASSWORD=ThisIsAnExamplePassword
# Logging
# Set to DEBUG level for dev only
LOGGING_LEVEL_HANDLERS_CONSOLE=ERROR
LOGGING_LEVEL_LOGGERS_ROOT=INFO
LOGGING_LEVEL_LOGGERS_APP=INFO
# Python
PYTHONPATH=/app
# impress settings
# Mail
DJANGO_EMAIL_BRAND_NAME="La Suite Numérique"
DJANGO_EMAIL_HOST="mailcatcher"
DJANGO_EMAIL_LOGO_IMG="https://impress.127.0.0.1.nip.io/assets/logo-suite-numerique.png"
DJANGO_EMAIL_PORT=1025
# Media
STORAGES_STATICFILES_BACKEND=django.contrib.staticfiles.storage.StaticFilesStorage
AWS_S3_ENDPOINT_URL=http://minio:9000
AWS_S3_ACCESS_KEY_ID=<minio root user>
AWS_S3_SECRET_ACCESS_KEY=<minio root password>
AWS_STORAGE_BUCKET_NAME=docs-media-storage
MEDIA_BASE_URL=impress.127.0.0.1.nip.io
# OIDC
USER_OIDC_FIELD_TO_SHORTNAME="given_name"
USER_OIDC_FIELDS_TO_FULLNAME="given_name,usual_name"
OIDC_OP_JWKS_ENDPOINT=https://impress.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT=https://impress.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT=https://impress.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT=https://impress.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT=https://impress.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_RP_CLIENT_ID=impress
OIDC_RP_CLIENT_SECRETThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO=RS256
OIDC_RP_SCOPES="openid email"
LOGIN_REDIRECT_URL=https://impress.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE=https://impress.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL=https://impress.127.0.0.1.nip.io
OIDC_REDIRECT_ALLOWED_HOSTS=["https://impress.127.0.0.1.nip.io"]
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# AI
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama
# Frontend
FRONTEND_THEME=dsfr
+9
View File
@@ -0,0 +1,9 @@
# Postgresql db container configuration
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=<Set postgresql password>
# Keycloak database configuration
KC_DB_URL_DATABASE=keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=<Same password as above>
+9
View File
@@ -0,0 +1,9 @@
KC_BOOTSTRAP_ADMIN_USERNAME=<Change this admin user>
KC_BOOTSTRAP_ADMIN_PASSWORD=<Change this admin password>
KC_DB=postgres
KC_DB_URL_HOST=kc_postgresql
KC_DB_SCHEMA=public
PROXY_ADDRESS_FORWARDING='true'
KC_HOSTNAME=http://localhost:8083
KC_HTTPS_CERTIFICATE_FILE=/etc/ssl/certs/docs.crt
KC_HTTPS_CERTIFICATE_KEY_FILE=/etc/ssl/private/docs.key
+2
View File
@@ -0,0 +1,2 @@
MINIO_ROOT_USER=<Set minio root username>
MINIO_ROOT_PASSWORD=<Set minio root password>
+11
View File
@@ -0,0 +1,11 @@
# Postgresql db container configuration
POSTGRES_DB=docs
POSTGRES_USER=docs
POSTGRES_PASSWORD=<Set postgresql password>
# App database configuration
DB_HOST=postgresql
DB_NAME=docs
DB_USER=docs
DB_PASSWORD=<Same password as above>
DB_PORT=5432
+5
View File
@@ -0,0 +1,5 @@
COLLABORATION_LOGGING=true
Y_PROVIDER_API_KEY=<Set y provider api key>
COLLABORATION_API_URL=https://impress.127.0.0.1.nip.io/collaboration/api/
COLLABORATION_SERVER_ORIGIN=https://impress.127.0.0.1.nip.io
COLLABORATION_SERVER_SECRET=<Set collaboration secret>
+21 -11
View File
@@ -201,7 +201,7 @@ class DocumentSerializer(ListDocumentSerializer):
"abilities",
"created_at",
"creator",
"is_avorite",
"is_favorite",
"link_role",
"link_reach",
"nb_accesses",
@@ -264,13 +264,17 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
"""Create the document and associate it with the user or send an invitation."""
language = validated_data.get("language", settings.LANGUAGE_CODE)
# Get the user based on the sub (unique identifier)
# Get the user on its sub (unique identifier). Default on email if allowed in settings
email = validated_data["email"]
try:
user = models.User.objects.get(sub=validated_data["sub"])
except (models.User.DoesNotExist, KeyError):
user = None
email = validated_data["email"]
else:
user = models.User.objects.get_user_by_sub_or_email(
validated_data["sub"], email
)
except models.DuplicateEmailError as err:
raise serializers.ValidationError({"email": [err.message]}) from err
if user:
email = user.email
language = user.language or language
@@ -279,7 +283,9 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
validated_data["content"]
)
except ConversionError as err:
raise exceptions.APIException(detail="could not convert content") from err
raise serializers.ValidationError(
{"content": ["Could not convert content"]}
) from err
document = models.Document.objects.create(
title=validated_data["title"],
@@ -302,7 +308,11 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
role=models.RoleChoices.OWNER,
)
# Notify the user about the newly created document
self._send_email_notification(document, validated_data, email, language)
return document
def _send_email_notification(self, document, validated_data, email, language):
"""Notify the user about the newly created document."""
subject = validated_data.get("subject") or _(
"A new document was created on your behalf!"
)
@@ -313,8 +323,6 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
}
document.send_email(subject, [email], context, language)
return document
def update(self, instance, validated_data):
"""
This serializer does not support updates.
@@ -380,6 +388,7 @@ class FileUploadSerializer(serializers.Serializer):
raise serializers.ValidationError("Could not determine file extension.")
self.context["expected_extension"] = extension
self.context["content_type"] = magic_mime_type
return file
@@ -387,6 +396,7 @@ class FileUploadSerializer(serializers.Serializer):
"""Override validate to add the computed extension to validated_data."""
attrs["expected_extension"] = self.context["expected_extension"]
attrs["is_unsafe"] = self.context["is_unsafe"]
attrs["content_type"] = self.context["content_type"]
return attrs
+6 -2
View File
@@ -605,7 +605,10 @@ class DocumentViewSet(
key = f"{document.key_base}/{ATTACHMENTS_FOLDER:s}/{file_id!s}.{extension:s}"
# Prepare metadata for storage
extra_args = {"Metadata": {"owner": str(request.user.id)}}
extra_args = {
"Metadata": {"owner": str(request.user.id)},
"ContentType": serializer.validated_data["content_type"],
}
if serializer.validated_data["is_unsafe"]:
extra_args["Metadata"]["is_unsafe"] = "true"
@@ -676,7 +679,7 @@ class DocumentViewSet(
# Fetch the document and check if the user has access
try:
document, _created = models.Document.objects.get_or_create(pk=pk)
document = models.Document.objects.get(pk=pk)
except models.Document.DoesNotExist as exc:
logger.debug("Document with ID '%s' does not exist", pk)
raise drf.exceptions.PermissionDenied() from exc
@@ -1124,6 +1127,7 @@ class ConfigView(drf.views.APIView):
"ENVIRONMENT",
"FRONTEND_THEME",
"MEDIA_BASE_URL",
"POSTHOG_KEY",
"LANGUAGES",
"LANGUAGE_CODE",
"SENTRY_DSN",
+5 -12
View File
@@ -11,7 +11,7 @@ from mozilla_django_oidc.auth import (
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
)
from core.models import User
from core.models import DuplicateEmailError, User
logger = logging.getLogger(__name__)
@@ -98,7 +98,10 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
"short_name": short_name,
}
user = self.get_existing_user(sub, email)
try:
user = User.objects.get_user_by_sub_or_email(sub, email)
except DuplicateEmailError as err:
raise SuspiciousOperation(err.message) from err
if user:
if not user.is_active:
@@ -117,16 +120,6 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
)
return full_name or None
def get_existing_user(self, sub, email):
"""Fetch an existing user by sub (or email as a fallback respecting fallback setting."""
try:
return User.objects.get(sub=sub)
except User.DoesNotExist:
if email and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
return User.objects.filter(email=email).first()
return None
def update_user_if_needed(self, user, claims):
"""Update user claims if they have changed."""
has_changed = any(
@@ -0,0 +1,95 @@
"""Management command updating the metadata for all the files in the MinIO bucket."""
from django.core.files.storage import default_storage
from django.core.management.base import BaseCommand
import magic
from core.models import Document
# pylint: disable=too-many-locals, broad-exception-caught
class Command(BaseCommand):
"""Update the metadata for all the files in the MinIO bucket."""
help = __doc__
def handle(self, *args, **options):
"""Execute management command."""
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
mime_detector = magic.Magic(mime=True)
documents = Document.objects.all()
self.stdout.write(
f"[INFO] Found {documents.count()} documents. Starting ContentType fix..."
)
for doc in documents:
doc_id_str = str(doc.id)
prefix = f"{doc_id_str}/attachments/"
self.stdout.write(
f"[INFO] Processing attachments under prefix '{prefix}' ..."
)
continuation_token = None
total_updated = 0
while True:
list_kwargs = {"Bucket": bucket_name, "Prefix": prefix}
if continuation_token:
list_kwargs["ContinuationToken"] = continuation_token
response = s3_client.list_objects_v2(**list_kwargs)
# If no objects found under this prefix, break out of the loop
if "Contents" not in response:
break
for obj in response["Contents"]:
key = obj["Key"]
# Skip if it's a folder
if key.endswith("/"):
continue
try:
# Get existing metadata
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
# Read first ~1KB for MIME detection
partial_obj = s3_client.get_object(
Bucket=bucket_name, Key=key, Range="bytes=0-1023"
)
partial_data = partial_obj["Body"].read()
# Detect MIME type
magic_mime_type = mime_detector.from_buffer(partial_data)
# Update ContentType
s3_client.copy_object(
Bucket=bucket_name,
CopySource={"Bucket": bucket_name, "Key": key},
Key=key,
ContentType=magic_mime_type,
Metadata=head_resp.get("Metadata", {}),
MetadataDirective="REPLACE",
)
total_updated += 1
except Exception as exc: # noqa
self.stderr.write(
f"[ERROR] Could not update ContentType for {key}: {exc}"
)
if response.get("IsTruncated"):
continuation_token = response.get("NextContinuationToken")
else:
break
if total_updated > 0:
self.stdout.write(
f"[INFO] -> Updated {total_updated} objects for Document {doc_id_str}."
)
+45 -2
View File
@@ -1,6 +1,7 @@
"""
Declare and configure the models for the impress core application
"""
# pylint: disable=too-many-lines
import hashlib
import smtplib
@@ -89,6 +90,16 @@ class LinkReachChoices(models.TextChoices):
PUBLIC = "public", _("Public") # Even anonymous users can access the document
class DuplicateEmailError(Exception):
"""Raised when an email is already associated with a pre-existing user."""
def __init__(self, message=None, email=None):
"""Set message and email to describe the exception."""
self.message = message
self.email = email
super().__init__(self.message)
class BaseModel(models.Model):
"""
Serves as an abstract base model for other models, ensuring that records are validated
@@ -126,6 +137,35 @@ class BaseModel(models.Model):
super().save(*args, **kwargs)
class UserManager(auth_models.UserManager):
"""Custom manager for User model with additional methods."""
def get_user_by_sub_or_email(self, sub, email):
"""Fetch existing user by sub or email."""
try:
return self.get(sub=sub)
except self.model.DoesNotExist as err:
if not email:
return None
if settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
try:
return self.get(email=email)
except self.model.DoesNotExist:
pass
elif (
self.filter(email=email).exists()
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
):
raise DuplicateEmailError(
_(
"We couldn't find a user with this sub but the email is already "
"associated with a registered user."
)
) from err
return None
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
"""User model to work with OIDC only authentication."""
@@ -192,7 +232,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
),
)
objects = auth_models.UserManager()
objects = UserManager()
USERNAME_FIELD = "admin_email"
REQUIRED_FIELDS = []
@@ -939,7 +979,10 @@ class Invitation(BaseModel):
super().clean()
# Check if an identity already exists for the provided email
if User.objects.filter(email=self.email).exists():
if (
User.objects.filter(email=self.email).exists()
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
):
raise exceptions.ValidationError(
{"email": _("This email is already associated to a registered user.")}
)
@@ -1,5 +1,6 @@
"""Unit tests for the Authentication Backends."""
import random
import re
from logging import Logger
from unittest import mock
@@ -64,7 +65,33 @@ def test_authentication_getter_existing_user_via_email(
assert user == db_user
def test_authentication_getter_existing_user_no_fallback_to_email(
def test_authentication_getter_email_none(monkeypatch):
"""
If no user is found with the sub and no email is provided, a new user should be created.
"""
klass = OIDCAuthenticationBackend()
db_user = UserFactory(email=None)
def get_userinfo_mocked(*args):
user_info = {"sub": "123"}
if random.choice([True, False]):
user_info["email"] = None
return user_info
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
# Since the sub and email didn't match, it should create a new user
assert models.User.objects.count() == 2
assert user != db_user
assert user.sub == "123"
def test_authentication_getter_existing_user_no_fallback_to_email_allow_duplicate(
settings, monkeypatch
):
"""
@@ -77,6 +104,7 @@ def test_authentication_getter_existing_user_no_fallback_to_email(
# Set the setting to False
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
settings.OIDC_ALLOW_DUPLICATE_EMAILS = True
def get_userinfo_mocked(*args):
return {"sub": "123", "email": db_user.email}
@@ -93,6 +121,39 @@ def test_authentication_getter_existing_user_no_fallback_to_email(
assert user.sub == "123"
def test_authentication_getter_existing_user_no_fallback_to_email_no_duplicate(
settings, monkeypatch
):
"""
When the "OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION" setting is set to False,
the system should not match users by email, even if the email matches.
"""
klass = OIDCAuthenticationBackend()
db_user = UserFactory()
# Set the setting to False
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
settings.OIDC_ALLOW_DUPLICATE_EMAILS = False
def get_userinfo_mocked(*args):
return {"sub": "123", "email": db_user.email}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with pytest.raises(
SuspiciousOperation,
match=(
"We couldn't find a user with this sub but the email is already associated "
"with a registered user."
),
):
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
# Since the sub doesn't match, it should not create a new user
assert models.User.objects.count() == 1
def test_authentication_getter_existing_user_with_email(
django_assert_num_queries, monkeypatch
):
@@ -0,0 +1,50 @@
"""
Unit test for `update_files_content_type_metadata` command.
"""
import uuid
from django.core.files.storage import default_storage
from django.core.management import call_command
import pytest
from core import factories
@pytest.mark.django_db
def test_update_files_content_type_metadata():
"""
Test that the command `update_files_content_type_metadata`
fixes the ContentType of attachment in the storage.
"""
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
# Create files with a wrong ContentType
keys = []
for _ in range(10):
doc_id = uuid.uuid4()
factories.DocumentFactory(id=doc_id)
key = f"{doc_id}/attachments/testfile.png"
keys.append(key)
fake_png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR..."
s3_client.put_object(
Bucket=bucket_name,
Key=key,
Body=fake_png,
ContentType="text/plain",
Metadata={"owner": "None"},
)
# Call the command that fixes the ContentType
call_command("update_files_content_type_metadata")
for key in keys:
head_resp = s3_client.head_object(Bucket=bucket_name, Key=key)
assert (
head_resp["ContentType"] == "image/png"
), f"ContentType not fixed, got {head_resp['ContentType']!r}"
# Check that original metadata was preserved
assert head_resp["Metadata"].get("owner") == "None"
@@ -64,12 +64,22 @@ def test_api_documents_attachment_upload_anonymous_success():
assert response.status_code == 201
pattern = re.compile(rf"^/media/{document.id!s}/attachments/(.*)\.png")
match = pattern.search(response.json()["file"])
file_path = response.json()["file"]
match = pattern.search(file_path)
file_id = match.group(1)
# Validate that file_id is a valid UUID
uuid.UUID(file_id)
# Now, check the metadata of the uploaded file
key = file_path.replace("/media", "")
file_head = default_storage.connection.meta.client.head_object(
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": "None"}
assert file_head["ContentType"] == "image/png"
@pytest.mark.parametrize(
"reach, role",
@@ -206,6 +216,7 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": str(user.id)}
assert file_head["ContentType"] == "image/png"
def test_api_documents_attachment_upload_invalid(client):
@@ -247,16 +258,18 @@ def test_api_documents_attachment_upload_size_limit_exceeded(settings):
@pytest.mark.parametrize(
"name,content,extension",
"name,content,extension,content_type",
[
("test.exe", b"text", "exe"),
("test", b"text", "txt"),
("test.aaaaaa", b"test", "txt"),
("test.txt", PIXEL, "txt"),
("test.py", b"#!/usr/bin/python", "py"),
("test.exe", b"text", "exe", "text/plain"),
("test", b"text", "txt", "text/plain"),
("test.aaaaaa", b"test", "txt", "text/plain"),
("test.txt", PIXEL, "txt", "image/png"),
("test.py", b"#!/usr/bin/python", "py", "text/plain"),
],
)
def test_api_documents_attachment_upload_fix_extension(name, content, extension):
def test_api_documents_attachment_upload_fix_extension(
name, content, extension, content_type
):
"""
A file with no extension or a wrong extension is accepted and the extension
is corrected in storage.
@@ -287,6 +300,7 @@ def test_api_documents_attachment_upload_fix_extension(name, content, extension)
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
assert file_head["ContentType"] == content_type
def test_api_documents_attachment_upload_empty_file():
@@ -335,3 +349,4 @@ def test_api_documents_attachment_upload_unsafe():
Bucket=default_storage.bucket_name, Key=key
)
assert file_head["Metadata"] == {"owner": str(user.id), "is_unsafe": "true"}
assert file_head["ContentType"] == "application/octet-stream"
@@ -13,6 +13,7 @@ import pytest
from rest_framework.test import APIClient
from core import factories
from core.api.serializers import ServerCreateDocumentSerializer
from core.models import Document, Invitation, User
from core.services.converter_services import ConversionError, YdocConverter
@@ -20,7 +21,7 @@ pytestmark = pytest.mark.django_db
@pytest.fixture
def mock_convert_markdown():
def mock_convert_md():
"""Mock YdocConverter.convert_markdown to return a converted content."""
with patch.object(
YdocConverter,
@@ -169,8 +170,11 @@ def test_api_documents_create_for_owner_invalid_sub():
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
def test_api_documents_create_for_owner_existing(mock_convert_markdown):
"""It should be possible to create a document on behalf of a pre-existing user."""
def test_api_documents_create_for_owner_existing(mock_convert_md):
"""
It should be possible to create a document on behalf of a pre-existing user
by passing their sub and email.
"""
user = factories.UserFactory(language="en-us")
data = {
@@ -189,7 +193,7 @@ def test_api_documents_create_for_owner_existing(mock_convert_markdown):
assert response.status_code == 201
mock_convert_markdown.assert_called_once_with("Document content")
mock_convert_md.assert_called_once_with("Document content")
document = Document.objects.get()
assert response.json() == {"id": str(document.id)}
@@ -213,10 +217,10 @@ def test_api_documents_create_for_owner_existing(mock_convert_markdown):
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
def test_api_documents_create_for_owner_new_user(mock_convert_markdown):
def test_api_documents_create_for_owner_new_user(mock_convert_md):
"""
It should be possible to create a document on behalf of new users by
passing only their email address.
passing their unknown sub and email address.
"""
data = {
"title": "My Document",
@@ -234,7 +238,7 @@ def test_api_documents_create_for_owner_new_user(mock_convert_markdown):
assert response.status_code == 201
mock_convert_markdown.assert_called_once_with("Document content")
mock_convert_md.assert_called_once_with("Document content")
document = Document.objects.get()
assert response.json() == {"id": str(document.id)}
@@ -264,8 +268,190 @@ def test_api_documents_create_for_owner_new_user(mock_convert_markdown):
assert document.creator == user
@override_settings(
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=True,
)
def test_api_documents_create_for_owner_existing_user_email_no_sub_with_fallback(
mock_convert_md,
):
"""
It should be possible to create a document on behalf of a pre-existing user for
who the sub was not found if the settings allow it. This edge case should not
happen in a healthy OIDC federation but can be usefull if an OIDC provider modifies
users sub on each login for example...
"""
user = factories.UserFactory(language="en-us")
data = {
"title": "My Document",
"content": "Document content",
"sub": "123",
"email": user.email,
}
response = APIClient().post(
"/api/v1.0/documents/create-for-owner/",
data,
format="json",
HTTP_AUTHORIZATION="Bearer DummyToken",
)
assert response.status_code == 201
mock_convert_md.assert_called_once_with("Document content")
document = Document.objects.get()
assert response.json() == {"id": str(document.id)}
assert document.title == "My Document"
assert document.content == "Converted document content"
assert document.creator == user
assert document.accesses.filter(user=user, role="owner").exists()
assert Invitation.objects.exists() is False
assert len(mail.outbox) == 1
email = mail.outbox[0]
assert email.to == [user.email]
assert email.subject == "A new document was created on your behalf!"
email_content = " ".join(email.body.split())
assert "A new document was created on your behalf!" in email_content
assert (
"You have been granted ownership of a new document: My Document"
) in email_content
@override_settings(
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=False,
OIDC_ALLOW_DUPLICATE_EMAILS=False,
)
def test_api_documents_create_for_owner_existing_user_email_no_sub_no_fallback(
mock_convert_md,
):
"""
When a user does not match an existing sub and fallback to matching on email is
not allowed in settings, it should raise an error if the email is already used by
a registered user and duplicate emails are not allowed.
"""
user = factories.UserFactory()
data = {
"title": "My Document",
"content": "Document content",
"sub": "123",
"email": user.email,
}
response = APIClient().post(
"/api/v1.0/documents/create-for-owner/",
data,
format="json",
HTTP_AUTHORIZATION="Bearer DummyToken",
)
assert response.status_code == 400
assert response.json() == {
"email": [
(
"We couldn't find a user with this sub but the email is already "
"associated with a registered user."
)
]
}
assert mock_convert_md.called is False
assert Document.objects.exists() is False
assert Invitation.objects.exists() is False
assert len(mail.outbox) == 0
@override_settings(
SERVER_TO_SERVER_API_TOKENS=["DummyToken"],
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION=False,
OIDC_ALLOW_DUPLICATE_EMAILS=True,
)
def test_api_documents_create_for_owner_new_user_no_sub_no_fallback_allow_duplicate(
mock_convert_md,
):
"""
When a user does not match an existing sub and fallback to matching on email is
not allowed in settings, it should be possible to create a new user with the same
email as an existing user if the settings allow it (identification is still done
via the sub in this case).
"""
user = factories.UserFactory()
data = {
"title": "My Document",
"content": "Document content",
"sub": "123",
"email": user.email,
}
response = APIClient().post(
"/api/v1.0/documents/create-for-owner/",
data,
format="json",
HTTP_AUTHORIZATION="Bearer DummyToken",
)
assert response.status_code == 201
mock_convert_md.assert_called_once_with("Document content")
document = Document.objects.get()
assert response.json() == {"id": str(document.id)}
assert document.title == "My Document"
assert document.content == "Converted document content"
assert document.creator is None
assert document.accesses.exists() is False
invitation = Invitation.objects.get()
assert invitation.email == user.email
assert invitation.role == "owner"
assert len(mail.outbox) == 1
email = mail.outbox[0]
assert email.to == [user.email]
assert email.subject == "A new document was created on your behalf!"
email_content = " ".join(email.body.split())
assert "A new document was created on your behalf!" in email_content
assert (
"You have been granted ownership of a new document: My Document"
) in email_content
# The creator field on the document should be set when the user is created
user = User.objects.create(email=user.email, password="!")
document.refresh_from_db()
assert document.creator == user
@patch.object(ServerCreateDocumentSerializer, "_send_email_notification")
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"], LANGUAGE_CODE="de-de")
def test_api_documents_create_for_owner_with_default_language(
mock_send, mock_convert_md
):
"""The default language from settings should apply by default."""
data = {
"title": "My Document",
"content": "Document content",
"sub": "123",
"email": "john.doe@example.com",
}
response = APIClient().post(
"/api/v1.0/documents/create-for-owner/",
data,
format="json",
HTTP_AUTHORIZATION="Bearer DummyToken",
)
assert response.status_code == 201
mock_convert_md.assert_called_once_with("Document content")
assert mock_send.call_args[0][3] == "de-de"
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
def test_api_documents_create_for_owner_with_custom_language(mock_convert_markdown):
def test_api_documents_create_for_owner_with_custom_language(mock_convert_md):
"""
Test creating a document with a specific language.
Useful if the remote server knows the user's language.
@@ -287,7 +473,7 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_markdo
assert response.status_code == 201
mock_convert_markdown.assert_called_once_with("Document content")
mock_convert_md.assert_called_once_with("Document content")
assert len(mail.outbox) == 1
email = mail.outbox[0]
@@ -302,7 +488,7 @@ def test_api_documents_create_for_owner_with_custom_language(mock_convert_markdo
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
def test_api_documents_create_for_owner_with_custom_subject_and_message(
mock_convert_markdown,
mock_convert_md,
):
"""It should be possible to customize the subject and message of the invitation email."""
data = {
@@ -323,7 +509,7 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
assert response.status_code == 201
mock_convert_markdown.assert_called_once_with("Document content")
mock_convert_md.assert_called_once_with("Document content")
assert len(mail.outbox) == 1
email = mail.outbox[0]
@@ -336,11 +522,11 @@ def test_api_documents_create_for_owner_with_custom_subject_and_message(
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
def test_api_documents_create_for_owner_with_converter_exception(
mock_convert_markdown,
mock_convert_md,
):
"""It should be possible to customize the subject and message of the invitation email."""
"""In case of converter error, a 400 error should be raised."""
mock_convert_markdown.side_effect = ConversionError("Conversion failed")
mock_convert_md.side_effect = ConversionError("Conversion failed")
data = {
"title": "My Document",
@@ -357,8 +543,33 @@ def test_api_documents_create_for_owner_with_converter_exception(
format="json",
HTTP_AUTHORIZATION="Bearer DummyToken",
)
mock_convert_md.assert_called_once_with("Document content")
mock_convert_markdown.assert_called_once_with("Document content")
assert response.status_code == 400
assert response.json() == {"content": ["Could not convert content"]}
assert response.status_code == 500
assert response.json() == {"detail": "could not convert content"}
@override_settings(SERVER_TO_SERVER_API_TOKENS=["DummyToken"])
def test_api_documents_create_for_owner_with_empty_content():
"""The content should not be empty or a 400 error should be raised."""
data = {
"title": "My Document",
"content": " ",
"sub": "123",
"email": "john.doe@example.com",
}
response = APIClient().post(
"/api/v1.0/documents/create-for-owner/",
data,
format="json",
HTTP_AUTHORIZATION="Bearer DummyToken",
)
assert response.status_code == 400
assert response.json() == {
"content": [
"This field may not be blank.",
],
}
@@ -20,6 +20,7 @@ pytestmark = pytest.mark.django_db
CRISP_WEBSITE_ID="123",
FRONTEND_THEME="test-theme",
MEDIA_BASE_URL="http://testserver/",
POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"},
SENTRY_DSN="https://sentry.test/123",
)
@pytest.mark.parametrize("is_authenticated", [False, True])
@@ -41,5 +42,6 @@ def test_api_config(is_authenticated):
"LANGUAGES": [["en-us", "English"], ["fr-fr", "French"], ["de-de", "German"]],
"LANGUAGE_CODE": "en-us",
"MEDIA_BASE_URL": "http://testserver/",
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
"SENTRY_DSN": "https://sentry.test/123",
}
+30
View File
@@ -0,0 +1,30 @@
"""
Unit tests for the User model
"""
import pytest
from impress.settings import Base
def test_invalid_settings_oidc_email_configuration():
"""
The OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and OIDC_ALLOW_DUPLICATE_EMAILS settings
should not be both set to True simultaneously.
"""
class TestSettings(Base):
"""Fake test settings."""
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
OIDC_ALLOW_DUPLICATE_EMAILS = True
# The validation is performed during post_setup
with pytest.raises(ValueError) as excinfo:
TestSettings().post_setup()
# Check the exception message
assert str(excinfo.value) == (
"Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
"OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
)
+23
View File
@@ -390,6 +390,11 @@ class Base(Configuration):
None, environ_name="FRONTEND_THEME", environ_prefix=None
)
# Posthog
POSTHOG_KEY = values.DictValue(
None, environ_name="POSTHOG_KEY", environ_prefix=None
)
# Crisp
CRISP_WEBSITE_ID = values.Value(
None, environ_name="CRISP_WEBSITE_ID", environ_prefix=None
@@ -474,6 +479,15 @@ class Base(Configuration):
environ_prefix=None,
)
# WARNING: Enabling this setting allows multiple user accounts to share the same email
# address. This may cause security issues and is not recommended for production use when
# email is activated as fallback for identification (see previous setting).
OIDC_ALLOW_DUPLICATE_EMAILS = values.BooleanValue(
default=False,
environ_name="OIDC_ALLOW_DUPLICATE_EMAILS",
environ_prefix=None,
)
USER_OIDC_ESSENTIAL_CLAIMS = values.ListValue(
default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None
)
@@ -625,6 +639,15 @@ class Base(Configuration):
with sentry_sdk.configure_scope() as scope:
scope.set_extra("application", "backend")
if (
cls.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
and cls.OIDC_ALLOW_DUPLICATE_EMAILS
):
raise ValueError(
"Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
"OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
)
class Build(Base):
"""Settings used when the application is built.
Binary file not shown.
+143 -185
View File
@@ -1,9 +1,9 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-people\n"
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-17 15:50+0000\n"
"PO-Revision-Date: 2024-12-17 15:53\n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-16 19:53\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -11,384 +11,342 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-people\n"
"X-Crowdin-Project-ID: 637934\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: de\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 8\n"
"X-Crowdin-File-ID: 18\n"
#: core/admin.py:33
#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr "Persönliche Daten"
#: core/admin.py:46
#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr "Berechtigungen"
#: core/admin.py:58
#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr "Wichtige Daten"
#: core/api/filters.py:16
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
msgstr "Ersteller bin ich"
#: core/api/filters.py:19
#: build/lib/core/api/filters.py:19 core/api/filters.py:19
msgid "Favorite"
msgstr ""
msgstr "Favorit"
#: core/api/filters.py:22
#: build/lib/core/api/filters.py:22 core/api/filters.py:22
msgid "Title"
msgstr ""
msgstr "Titel"
#: core/api/serializers.py:307
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
msgid "A new document was created on your behalf!"
msgstr ""
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: core/api/serializers.py:311
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
msgid "You have been granted ownership of a new document:"
msgstr ""
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: core/api/serializers.py:414
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr "Inhalt"
#: core/api/serializers.py:417
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr "Typ"
#: core/api/serializers.py:423
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr "Format"
msgstr ""
#: core/authentication/backends.py:57
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr ""
msgstr "Ungültiges Antwortformat oder Token-Verifizierung fehlgeschlagen"
#: core/authentication/backends.py:81
msgid "User info contained no recognizable user identification"
msgstr ""
#: core/authentication/backends.py:88
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr ""
msgstr "Benutzerkonto ist deaktiviert"
#: core/models.py:62 core/models.py:69
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
msgid "Reader"
msgstr "Lesen"
#: core/models.py:63 core/models.py:70
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
msgid "Editor"
msgstr "Bearbeiten"
#: core/models.py:71
#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr "Administrator"
msgstr ""
#: core/models.py:72
#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr "Besitzer"
#: core/models.py:83
#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr "Beschränkt"
#: core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr "Authentifiziert"
#: core/models.py:89
#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr "Öffentlich"
#: core/models.py:101
#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
#: core/models.py:102
#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
#: core/models.py:108
#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr "Erstellt"
#: core/models.py:109
#: 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"
#: core/models.py:114
#: build/lib/core/models.py:125 core/models.py:125
msgid "updated on"
msgstr "Aktualisiert"
#: core/models.py:115
#: 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"
#: core/models.py:135
#: 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:175 core/models.py:175
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
#: core/models.py:141
#: build/lib/core/models.py:181 core/models.py:181
msgid "sub"
msgstr ""
msgstr "unter"
#: core/models.py:143
#: build/lib/core/models.py:183 core/models.py:183
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
#: core/models.py:152
#: build/lib/core/models.py:192 core/models.py:192
msgid "full name"
msgstr ""
msgstr "Name"
#: core/models.py:153
#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr ""
msgstr "Kurzbezeichnung"
#: core/models.py:155
#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr ""
msgstr "Identitäts-E-Mail-Adresse"
#: core/models.py:160
#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr ""
msgstr "Admin E-Mail-Adresse"
#: core/models.py:167
#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr "Sprache"
#: core/models.py:168
#: build/lib/core/models.py:208 core/models.py:208
msgid "The language in which the user wants to see the interface."
msgstr ""
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
#: core/models.py:174
#: build/lib/core/models.py:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr ""
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
#: core/models.py:177
#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr ""
msgstr "Gerät"
#: core/models.py:179
#: build/lib/core/models.py:219 core/models.py:219
msgid "Whether the user is a device or a real user."
msgstr ""
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
#: core/models.py:182
#: build/lib/core/models.py:222 core/models.py:222
msgid "staff status"
msgstr ""
msgstr "Status des Teammitgliedes"
#: core/models.py:184
#: build/lib/core/models.py:224 core/models.py:224
msgid "Whether the user can log into this admin site."
msgstr ""
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
#: core/models.py:187
#: build/lib/core/models.py:227 core/models.py:227
msgid "active"
msgstr ""
msgstr "aktiviert"
#: core/models.py:190
#: 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 ""
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
#: core/models.py:202
#: build/lib/core/models.py:242 core/models.py:242
msgid "user"
msgstr "Benutzer"
#: core/models.py:203
#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr "Benutzer"
#: core/models.py:342 core/models.py:718
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
msgid "title"
msgstr "Titel"
#: core/models.py:364
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
msgstr "Dokument"
#: core/models.py:365
#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr "Dokumente"
#: core/models.py:368
#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: core/models.py:593
#: build/lib/core/models.py:633 core/models.py:633
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: core/models.py:597
#: 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 ""
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
#: core/models.py:600
#: build/lib/core/models.py:640 core/models.py:640
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
#: core/models.py:623
#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr ""
msgstr "Dokument/Benutzer Linkverfolgung"
#: core/models.py:624
#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr ""
msgstr "Dokument/Benutzer Linkverfolgung"
#: core/models.py:630
#: build/lib/core/models.py:670 core/models.py:670
msgid "A link trace already exists for this document/user."
msgstr ""
#: core/models.py:653
#: build/lib/core/models.py:693 core/models.py:693
msgid "Document favorite"
msgstr ""
msgstr "Dokumentenfavorit"
#: core/models.py:654
#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr ""
msgstr "Dokumentfavoriten"
#: core/models.py:660
#: 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 ""
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
#: core/models.py:682
#: build/lib/core/models.py:722 core/models.py:722
msgid "Document/user relation"
msgstr ""
msgstr "Dokument/Benutzerbeziehung"
#: core/models.py:683
#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr ""
msgstr "Dokument/Benutzerbeziehungen"
#: core/models.py:689
#: 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."
#: core/models.py:695
#: 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."
#: core/models.py:701 core/models.py:890
#: 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."
#: core/models.py:719
#: build/lib/core/models.py:759 core/models.py:759
msgid "description"
msgstr "Beschreibung"
#: core/models.py:720
#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr "Code"
#: core/models.py:721
#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr "CSS"
#: core/models.py:723
#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr "öffentlich"
#: core/models.py:725
#: 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."
#: core/models.py:731
#: build/lib/core/models.py:771 core/models.py:771
msgid "Template"
msgstr ""
msgstr "Vorlage"
#: core/models.py:732
#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr ""
msgstr "Vorlagen"
#: core/models.py:871
#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr ""
msgstr "Vorlage/Benutzer-Beziehung"
#: core/models.py:872
#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr ""
msgstr "Vorlage/Benutzerbeziehungen"
#: core/models.py:878
#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr ""
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: core/models.py:884
#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr ""
msgstr "Dieses Team ist bereits in diesem Template."
#: core/models.py:907
#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr ""
msgstr "E-Mail-Adresse"
#: core/models.py:926
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr ""
msgstr "Einladung zum Dokument"
#: core/models.py:927
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr ""
msgstr "Dokumenteinladungen"
#: core/models.py:944
#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr ""
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
msgid "Company logo"
msgstr ""
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
#, python-format
msgid "Hello %(name)s"
msgstr ""
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
msgid "Hello"
msgstr ""
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
msgid "Thank you very much for your visit!"
msgstr ""
#: core/templates/mail/html/hello.html:221
#, python-format
msgid "This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
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 ""
#: core/templates/mail/text/hello.txt:8
#, python-format
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
msgstr ""
#: impress/settings.py:236
#: build/lib/impress/settings.py:236 impress/settings.py:236
msgid "English"
msgstr ""
msgstr "Englisch"
#: impress/settings.py:237
#: build/lib/impress/settings.py:237 impress/settings.py:237
msgid "French"
msgstr ""
msgstr "Französisch"
#: impress/settings.py:238
#: build/lib/impress/settings.py:238 impress/settings.py:238
msgid "German"
msgstr ""
msgstr "Deutsch"
Binary file not shown.
Binary file not shown.
+98 -140
View File
@@ -1,9 +1,9 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-people\n"
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-17 15:50+0000\n"
"PO-Revision-Date: 2024-12-17 15:53\n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-16 19:53\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -11,384 +11,342 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Crowdin-Project: lasuite-people\n"
"X-Crowdin-Project-ID: 637934\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: fr\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 8\n"
"X-Crowdin-File-ID: 18\n"
#: core/admin.py:33
#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr "Infos Personnelles"
#: core/admin.py:46
#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr "Permissions"
msgstr ""
#: core/admin.py:58
#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr "Dates importantes"
#: core/api/filters.py:16
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
#: core/api/filters.py:19
#: build/lib/core/api/filters.py:19 core/api/filters.py:19
msgid "Favorite"
msgstr ""
#: core/api/filters.py:22
#: build/lib/core/api/filters.py:22 core/api/filters.py:22
msgid "Title"
msgstr ""
#: core/api/serializers.py:307
#: 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 !"
#: core/api/serializers.py:311
#: 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 :"
#: core/api/serializers.py:414
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr ""
#: core/api/serializers.py:417
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr ""
#: core/api/serializers.py:423
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr ""
#: core/authentication/backends.py:57
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr ""
#: core/authentication/backends.py:81
msgid "User info contained no recognizable user identification"
msgstr ""
#: core/authentication/backends.py:88
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr ""
#: core/models.py:62 core/models.py:69
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
msgid "Reader"
msgstr "Lecteur"
#: core/models.py:63 core/models.py:70
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
msgid "Editor"
msgstr "Éditeur"
#: core/models.py:71
#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr "Administrateur"
#: core/models.py:72
#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr "Propriétaire"
#: core/models.py:83
#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr "Restreint"
#: core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr "Authentifié"
#: core/models.py:89
#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr "Public"
msgstr ""
#: core/models.py:101
#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
#: core/models.py:102
#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
#: core/models.py:108
#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr ""
#: core/models.py:109
#: build/lib/core/models.py:120 core/models.py:120
msgid "date and time at which a record was created"
msgstr ""
#: core/models.py:114
#: build/lib/core/models.py:125 core/models.py:125
msgid "updated on"
msgstr ""
#: core/models.py:115
#: build/lib/core/models.py:126 core/models.py:126
msgid "date and time at which a record was last updated"
msgstr ""
#: core/models.py:135
#: 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:175 core/models.py:175
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: core/models.py:141
#: build/lib/core/models.py:181 core/models.py:181
msgid "sub"
msgstr ""
#: core/models.py:143
#: build/lib/core/models.py:183 core/models.py:183
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: core/models.py:152
#: build/lib/core/models.py:192 core/models.py:192
msgid "full name"
msgstr ""
#: core/models.py:153
#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr ""
#: core/models.py:155
#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr ""
#: core/models.py:160
#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr ""
#: core/models.py:167
#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr ""
#: core/models.py:168
#: build/lib/core/models.py:208 core/models.py:208
msgid "The language in which the user wants to see the interface."
msgstr ""
#: core/models.py:174
#: build/lib/core/models.py:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr ""
#: core/models.py:177
#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr ""
#: core/models.py:179
#: build/lib/core/models.py:219 core/models.py:219
msgid "Whether the user is a device or a real user."
msgstr ""
#: core/models.py:182
#: build/lib/core/models.py:222 core/models.py:222
msgid "staff status"
msgstr ""
#: core/models.py:184
#: build/lib/core/models.py:224 core/models.py:224
msgid "Whether the user can log into this admin site."
msgstr ""
#: core/models.py:187
#: build/lib/core/models.py:227 core/models.py:227
msgid "active"
msgstr ""
#: core/models.py:190
#: 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 ""
#: core/models.py:202
#: build/lib/core/models.py:242 core/models.py:242
msgid "user"
msgstr ""
#: core/models.py:203
#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr ""
#: core/models.py:342 core/models.py:718
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
msgid "title"
msgstr ""
#: core/models.py:364
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
msgstr ""
#: core/models.py:365
#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr ""
#: core/models.py:368
#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr ""
#: core/models.py:593
#: 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!"
#: core/models.py:597
#: 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:"
#: core/models.py:600
#: 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}"
#: core/models.py:623
#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr ""
#: core/models.py:624
#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr ""
#: core/models.py:630
#: build/lib/core/models.py:670 core/models.py:670
msgid "A link trace already exists for this document/user."
msgstr ""
#: core/models.py:653
#: build/lib/core/models.py:693 core/models.py:693
msgid "Document favorite"
msgstr ""
#: core/models.py:654
#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr ""
#: core/models.py:660
#: 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 ""
#: core/models.py:682
#: build/lib/core/models.py:722 core/models.py:722
msgid "Document/user relation"
msgstr ""
#: core/models.py:683
#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr ""
#: core/models.py:689
#: build/lib/core/models.py:729 core/models.py:729
msgid "This user is already in this document."
msgstr ""
#: core/models.py:695
#: build/lib/core/models.py:735 core/models.py:735
msgid "This team is already in this document."
msgstr ""
#: core/models.py:701 core/models.py:890
#: 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 ""
#: core/models.py:719
#: build/lib/core/models.py:759 core/models.py:759
msgid "description"
msgstr ""
#: core/models.py:720
#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr ""
#: core/models.py:721
#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr ""
#: core/models.py:723
#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr ""
#: core/models.py:725
#: build/lib/core/models.py:765 core/models.py:765
msgid "Whether this template is public for anyone to use."
msgstr ""
#: core/models.py:731
#: build/lib/core/models.py:771 core/models.py:771
msgid "Template"
msgstr ""
#: core/models.py:732
#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr ""
#: core/models.py:871
#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr ""
#: core/models.py:872
#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr ""
#: core/models.py:878
#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr ""
#: core/models.py:884
#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr ""
#: core/models.py:907
#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr ""
#: core/models.py:926
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr ""
#: core/models.py:927
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr ""
#: core/models.py:944
#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
msgid "Company logo"
msgstr ""
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
#, python-format
msgid "Hello %(name)s"
msgstr ""
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
msgid "Hello"
msgstr ""
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
msgid "Thank you very much for your visit!"
msgstr ""
#: core/templates/mail/html/hello.html:221
#, python-format
msgid "This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
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 "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 "
#: core/templates/mail/text/hello.txt:8
#, python-format
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
msgstr ""
#: impress/settings.py:236
#: build/lib/impress/settings.py:236 impress/settings.py:236
msgid "English"
msgstr ""
#: impress/settings.py:237
#: build/lib/impress/settings.py:237 impress/settings.py:237
msgid "French"
msgstr ""
#: impress/settings.py:238
#: build/lib/impress/settings.py:238 impress/settings.py:238
msgid "German"
msgstr ""
@@ -0,0 +1,352 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-16 19:53\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: nl\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:46 core/admin.py:46
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:58 core/admin.py:58
msgid "Important dates"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:19 core/api/filters.py:19
msgid "Favorite"
msgstr ""
#: build/lib/core/api/filters.py:22 core/api/filters.py:22
msgid "Title"
msgstr ""
#: 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:321 core/api/serializers.py:321
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
msgid "Format"
msgstr ""
#: build/lib/core/authentication/backends.py:61
#: core/authentication/backends.py:61
msgid "Invalid response format or token verification failed"
msgstr ""
#: build/lib/core/authentication/backends.py:108
#: core/authentication/backends.py:108
msgid "User account is disabled"
msgstr ""
#: build/lib/core/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:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:72 core/models.py:72
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:73 core/models.py:73
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:84 core/models.py:84
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:90 core/models.py:90
msgid "Public"
msgstr ""
#: build/lib/core/models.py:112 core/models.py:112
msgid "id"
msgstr ""
#: build/lib/core/models.py:113 core/models.py:113
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:119 core/models.py:119
msgid "created on"
msgstr ""
#: 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:125 core/models.py:125
msgid "updated on"
msgstr ""
#: 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: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: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:181 core/models.py:181
msgid "sub"
msgstr ""
#: 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:192 core/models.py:192
msgid "full name"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
msgid "short name"
msgstr ""
#: build/lib/core/models.py:195 core/models.py:195
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:200 core/models.py:200
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:207 core/models.py:207
msgid "language"
msgstr ""
#: 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:214 core/models.py:214
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:217 core/models.py:217
msgid "device"
msgstr ""
#: 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:222 core/models.py:222
msgid "staff status"
msgstr ""
#: 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:227 core/models.py:227
msgid "active"
msgstr ""
#: 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:242 core/models.py:242
msgid "user"
msgstr ""
#: build/lib/core/models.py:243 core/models.py:243
msgid "users"
msgstr ""
#: 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:404 core/models.py:404
msgid "Document"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:408 core/models.py:408
msgid "Untitled Document"
msgstr ""
#: 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: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:640 core/models.py:640
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:663 core/models.py:663
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:664 core/models.py:664
msgid "Document/user link traces"
msgstr ""
#: 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:693 core/models.py:693
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:694 core/models.py:694
msgid "Document favorites"
msgstr ""
#: 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:722 core/models.py:722
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:723 core/models.py:723
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:729 core/models.py:729
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:735 core/models.py:735
msgid "This team is already in this document."
msgstr ""
#: 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:759 core/models.py:759
msgid "description"
msgstr ""
#: build/lib/core/models.py:760 core/models.py:760
msgid "code"
msgstr ""
#: build/lib/core/models.py:761 core/models.py:761
msgid "css"
msgstr ""
#: build/lib/core/models.py:763 core/models.py:763
msgid "public"
msgstr ""
#: 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:771 core/models.py:771
msgid "Template"
msgstr ""
#: build/lib/core/models.py:772 core/models.py:772
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:924 core/models.py:924
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:947 core/models.py:947
msgid "email address"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:987 core/models.py:987
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:236 impress/settings.py:236
msgid "English"
msgstr ""
#: build/lib/impress/settings.py:237 impress/settings.py:237
msgid "French"
msgstr ""
#: build/lib/impress/settings.py:238 impress/settings.py:238
msgid "German"
msgstr ""
+2 -2
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "1.10.0"
version = "2.0.1"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -37,7 +37,7 @@ dependencies = [
"django-redis==5.4.0",
"django-storages[s3]==1.14.4",
"django-timezone-field>=5.1",
"django==5.1.4",
"django==5.1.5",
"djangorestframework==3.15.2",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
@@ -16,6 +16,7 @@ const config = {
['de-de', 'German'],
],
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
SENTRY_DSN: null,
};
@@ -47,6 +47,7 @@ test.describe('Doc Header', () => {
versions_list: true,
versions_retrieve: true,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
@@ -394,7 +395,38 @@ 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 }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means owner
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
accesses_manage: false,
accesses_view: false,
update: true,
partial_update: true,
retrieve: true,
},
});
await goToGridDoc(page);
const shareButton = page.getByRole('button', {
name: 'Share',
exact: true,
});
await expect(shareButton).toBeVisible();
await shareButton.click();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible();
});
});
@@ -405,6 +437,46 @@ test.describe('Documents Header mobile', () => {
await page.goto('/');
});
test('it checks the copy link button', async ({ page, browserName }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(
browserName === 'webkit',
'navigator.clipboard is not working with webkit and playwright',
);
await mockedDocument(page, {
abilities: {
destroy: false,
link_configuration: true,
versions_destroy: true,
versions_list: true,
versions_retrieve: true,
accesses_manage: false,
accesses_view: false,
update: true,
partial_update: true,
retrieve: true,
},
});
await goToGridDoc(page);
await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden();
await page.getByLabel('Open the document options').click();
await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible();
// Test that clipboard is in HTML format
const handle = await page.evaluateHandle(() =>
navigator.clipboard.readText(),
);
const clipboardContent = await handle.jsonValue();
const origin = await page.evaluate(() => window.location.origin);
expect(clipboardContent.trim()).toMatch(
`${origin}/docs/mocked-document-id/`,
);
});
test('it checks the close button on Share modal', async ({ page }) => {
await mockedDocument(page, {
abilities: {
@@ -414,6 +486,7 @@ test.describe('Documents Header mobile', () => {
versions_list: true,
versions_retrieve: true,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
@@ -160,7 +160,7 @@ test.describe('Document create member', () => {
await page.getByRole('button', { name: 'Partager' }).click();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
name: 'Saisie de recherche rapide',
});
const email = randomName('test@test.fr', browserName, 1)[0];
@@ -232,6 +232,9 @@ test.describe('Doc Visibility: Public', () => {
cardContainer.getByText('Public document', { exact: true }),
).toBeVisible();
await expect(page.getByRole('button', { name: 'search' })).toBeVisible();
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
const urlDoc = page.url();
await page
@@ -245,7 +248,9 @@ test.describe('Doc Visibility: Public', () => {
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await expect(page.getByRole('button', { name: 'search' })).toBeHidden();
await expect(page.getByRole('button', { name: 'New doc' })).toBeHidden();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
const card = page.getByLabel('It is the card information');
await expect(card).toBeVisible();
@@ -309,7 +314,7 @@ test.describe('Doc Visibility: Public', () => {
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
});
});
@@ -409,13 +414,8 @@ test.describe('Doc Visibility: Authenticated', () => {
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await expect(selectVisibility).toBeHidden();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
await expect(inputSearch).toBeHidden();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible();
});
test('It checks a authenticated doc in editable mode', async ({
@@ -470,12 +470,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
await expect(selectVisibility).toBeHidden();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
await expect(inputSearch).toBeHidden();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible();
});
});
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "1.10.0",
"version": "2.0.1",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -13,12 +13,12 @@
},
"devDependencies": {
"@playwright/test": "1.49.1",
"@types/luxon": "3.4.2",
"@types/node": "*",
"@types/pdf-parse": "1.1.4",
"eslint-config-impress": "*",
"typescript": "*",
"luxon": "3.5.0",
"@types/luxon": "3.4.2"
"typescript": "*"
},
"dependencies": {
"convert-stream": "1.0.2",
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "1.10.0",
"version": "2.0.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -15,9 +15,9 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@blocknote/core": "0.22.0",
"@blocknote/mantine": "0.22.0",
"@blocknote/react": "0.22.0",
"@blocknote/core": "0.21.0",
"@blocknote/mantine": "0.21.0",
"@blocknote/react": "0.21.0",
"@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.15.0",
"@openfun/cunningham-react": "2.9.4",
@@ -31,6 +31,7 @@
"lodash": "4.17.21",
"luxon": "3.5.0",
"next": "15.1.3",
"posthog-js": "1.204.0",
"react": "*",
"react-aria-components": "1.5.0",
"react-dom": "*",
@@ -20,12 +20,14 @@ export type DropdownMenuProps = {
showArrow?: boolean;
label?: string;
arrowCss?: BoxProps['$css'];
disabled?: boolean;
topMessage?: string;
};
export const DropdownMenu = ({
options,
children,
disabled = false,
showArrow = false,
arrowCss,
label,
@@ -40,6 +42,10 @@ export const DropdownMenu = ({
setIsOpen(isOpen);
};
if (disabled) {
return children;
}
return (
<DropButton
isOpen={isOpen}
@@ -1,8 +1,8 @@
import Link from 'next/link';
import styled from 'styled-components';
import styled, { RuleSet } from 'styled-components';
export interface LinkProps {
$css?: string;
$css?: string | RuleSet<object>;
}
export const StyledLink = styled(Link)<LinkProps>`
@@ -12,5 +12,5 @@ export const StyledLink = styled(Link)<LinkProps>`
color: #ffffff;
}
display: flex;
${({ $css }) => $css && `${$css};`}
${({ $css }) => $css && (typeof $css === 'string' ? `${$css};` : $css)}
`;
@@ -3,7 +3,7 @@ import { PropsWithChildren, useEffect } from 'react';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { configureCrispSession } from '@/services';
import { PostHogProvider, configureCrispSession } from '@/services';
import { useSentryStore } from '@/stores/useSentryStore';
import { useConfig } from './api/useConfig';
@@ -45,5 +45,5 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
);
}
return children;
return <PostHogProvider conf={conf.POSTHOG_KEY}>{children}</PostHogProvider>;
};
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Theme } from '@/cunningham/';
import { PostHogConf } from '@/services';
interface ConfigResponse {
LANGUAGES: [string, string][];
@@ -11,6 +12,7 @@ interface ConfigResponse {
CRISP_WEBSITE_ID?: string;
FRONTEND_THEME?: Theme;
MEDIA_BASE_URL?: string;
POSTHOG_KEY?: PostHogConf;
SENTRY_DSN?: string;
}
@@ -576,6 +576,22 @@ input:-webkit-autofill:focus {
}
}
.c__modal__scroller:has(.noPadding) {
padding: 0 !important;
.c__modal__close .c__button {
right: 5px;
top: 5px;
padding: 1.5rem 1rem;
}
.c__modal__title {
font-size: var(--c--theme--font--sizes--xs);
padding: var(--c--theme--spacings--base);
margin-bottom: 0;
}
}
/**
* Toast
*/
@@ -6,6 +6,7 @@ import { useCreateBlockNote } from '@blocknote/react';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
@@ -20,17 +21,19 @@ import { randomColor } from '../utils';
import { BlockNoteToolbar } from './BlockNoteToolbar';
const cssEditor = (readonly: boolean) => `
&, & > .bn-container, & .ProseMirror {
height:100%;
.bn-side-menu[data-block-type=heading][data-level="1"] {
height: 50px;
}
.bn-side-menu[data-block-type=heading][data-level="2"] {
height: 43px;
}
.bn-side-menu[data-block-type=heading][data-level="3"] {
const cssEditor = (readonly: boolean) => css`
&,
& > .bn-container,
& .ProseMirror {
height: 100%;
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 50px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
h1 {
@@ -52,11 +55,11 @@ const cssEditor = (readonly: boolean) => `
border-left: none;
}
}
.bn-editor {
color: var(--c--theme--colors--greyscale-700);
}
.bn-block-outer:not(:first-child) {
&:has(h1) {
padding-top: 32px;
@@ -67,25 +70,25 @@ const cssEditor = (readonly: boolean) => `
&:has(h3) {
padding-top: 16px;
}
};
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 560px) {
& .bn-editor {
${readonly && `padding-left: 10px;`}
};
.bn-side-menu[data-block-type=heading][data-level="1"] {
height: 46px;
}
.bn-side-menu[data-block-type=heading][data-level="2"] {
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type=heading][data-level="3"] {
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
& .bn-editor h1 {
@@ -97,7 +100,7 @@ const cssEditor = (readonly: boolean) => `
& .bn-editor h3 {
font-size: 1.2rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type="paragraph"]
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
@@ -176,7 +179,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
}, [setEditor, editor]);
return (
<Box $css={cssEditor(readOnly)}>
<Box
$padding={{ top: 'md' }}
$background="white"
$css={cssEditor(readOnly)}
>
{errorAttachment && (
<Box $margin={{ bottom: 'big' }}>
<TextErrors
@@ -42,7 +42,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<Box
aria-label={t('Public document')}
$color={colors['primary-800']}
$background={colors['primary-100']}
$background={colors['primary-050']}
$radius={spacings['3xs']}
$direction="row"
$padding="xs"
@@ -64,7 +64,12 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
</Text>
</Box>
)}
<Box $direction="row" $align="center" $width="100%">
<Box
$direction="row"
$align="center"
$width="100%"
$padding={{ bottom: 'xs' }}
>
<Box
$direction="row"
$justify="space-between"
@@ -98,7 +103,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<DocToolBox doc={doc} />
</Box>
</Box>
<HorizontalSeparator $withPadding={true} />
<HorizontalSeparator $withPadding={false} />
</Box>
</>
);
@@ -16,7 +16,6 @@ import {
Icon,
IconOptions,
} from '@/components';
import { useAuthStore } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { useEditorStore } from '@/features/docs/doc-editor/';
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
@@ -37,6 +36,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation();
const hasAccesses = doc.nb_accesses > 1;
const queryClient = useQueryClient();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const spacings = spacingsTokens();
@@ -48,7 +48,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const modalShare = useModal();
const { isSmallMobile, isDesktop } = useResponsiveStore();
const { authenticated } = useAuthStore();
const { editor } = useEditorStore();
const { toast } = useToastProvider();
@@ -57,10 +56,8 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
? [
{
label: t('Share'),
icon: 'upload',
callback: () => {
modalShare.open();
},
icon: 'group',
callback: modalShare.open,
},
{
label: t('Export'),
@@ -153,7 +150,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$margin={{ left: 'auto' }}
$gap={spacings['2xs']}
>
{authenticated && !isSmallMobile && (
{!isSmallMobile && (
<>
{!hasAccesses && (
<Button
@@ -193,6 +190,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
)}
</>
)}
{!isSmallMobile && (
<Button
color="tertiary-text"
@@ -1,2 +1,3 @@
export * from './useCollaboration';
export * from './useTrans';
export * from './useCopyDocLink';
@@ -0,0 +1,19 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useClipboard } from '@/hook';
import { Doc } from '../types';
export const useCopyDocLink = (docId: Doc['id']) => {
const { t } = useTranslation();
const copyToClipboard = useClipboard();
return useCallback(() => {
copyToClipboard(
`${window.location.origin}/docs/${docId}/`,
t('Link Copied !'),
t('Failed to copy link'),
);
}, [copyToClipboard, docId, t]);
};
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { createGlobalStyle, css } from 'styled-components';
import { useDebouncedCallback } from 'use-debounce';
import { Box, LoadMoreText } from '@/components';
import { Box, HorizontalSeparator, LoadMoreText, Text } from '@/components';
import {
QuickSearch,
QuickSearchData,
@@ -58,6 +58,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
const [listHeight, setListHeight] = useState<string>('400px');
const canShare = doc.abilities.accesses_manage;
const canViewAccesses = doc.abilities.accesses_view;
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
const showFooter = selectedUsers.length === 0 && !inputValue;
@@ -137,7 +138,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
};
return {
groupName: t('Search user result', { count: users.length }),
groupName: t('Search user result'),
elements: users,
endActions:
isEmail && users.length === 0
@@ -191,8 +192,9 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
<ShareModalStyle />
<Box
aria-label={t('Share modal')}
$height={modalContentHeight}
$height={canViewAccesses ? modalContentHeight : 'auto'}
$overflow="hidden"
className="noPadding"
$justify="space-between"
>
<Box
@@ -204,7 +206,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
}
`}
>
<div ref={selectedUsersRef}>
<Box ref={selectedUsersRef}>
{canShare && selectedUsers.length > 0 && (
<Box
$padding={{ horizontal: 'base' }}
@@ -222,55 +224,76 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
/>
</Box>
)}
</div>
{!canViewAccesses && <HorizontalSeparator />}
</Box>
<Box data-testid="doc-share-quick-search">
<QuickSearch
onFilter={(str) => {
setInputValue(str);
onFilter(str);
}}
inputValue={inputValue}
showInput={canShare}
loading={searchUsersQuery.isLoading}
placeholder={t('Type a name or email')}
>
{!showMemberSection && inputValue !== '' && (
<QuickSearchGroup
group={searchUserData}
onSelect={onSelect}
renderElement={(user) => (
<DocShareModalInviteUserRow user={user} />
{!canViewAccesses && (
<Box $height={listHeight} $align="center" $justify="center">
<Text
$maxWidth="320px"
$textAlign="center"
$variation="600"
$size="sm"
>
{t(
'You do not have permission to view users sharing this document or modify link settings.',
)}
/>
)}
{showMemberSection && (
<>
{invitationsData.elements.length > 0 && (
<Box aria-label={t('List invitation card')}>
</Text>
</Box>
)}
{canViewAccesses && (
<QuickSearch
onFilter={(str) => {
setInputValue(str);
onFilter(str);
}}
inputValue={inputValue}
showInput={canShare}
loading={searchUsersQuery.isLoading}
placeholder={t('Type a name or email')}
>
{canViewAccesses && (
<>
{!showMemberSection && inputValue !== '' && (
<QuickSearchGroup
group={invitationsData}
renderElement={(invitation) => (
<DocShareInvitationItem
doc={doc}
invitation={invitation}
/>
group={searchUserData}
onSelect={onSelect}
renderElement={(user) => (
<DocShareModalInviteUserRow user={user} />
)}
/>
</Box>
)}
)}
{showMemberSection && (
<>
{invitationsData.elements.length > 0 && (
<Box aria-label={t('List invitation card')}>
<QuickSearchGroup
group={invitationsData}
renderElement={(invitation) => (
<DocShareInvitationItem
doc={doc}
invitation={invitation}
/>
)}
/>
</Box>
)}
<Box aria-label={t('List members card')}>
<QuickSearchGroup
group={membersData}
renderElement={(access) => (
<DocShareMemberItem doc={doc} access={access} />
)}
/>
</Box>
</>
)}
</QuickSearch>
<Box aria-label={t('List members card')}>
<QuickSearchGroup
group={membersData}
renderElement={(access) => (
<DocShareMemberItem doc={doc} access={access} />
)}
/>
</Box>
</>
)}
</>
)}
</QuickSearch>
)}
</Box>
</Box>
@@ -1,13 +1,9 @@
import {
Button,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, HorizontalSeparator } from '@/components';
import { Doc } from '@/features/docs';
import { Doc, useCopyDocLink } from '@/features/docs';
import { DocVisibility } from './DocVisibility';
@@ -17,8 +13,7 @@ type Props = {
};
export const DocShareModalFooter = ({ doc, onClose }: Props) => {
const canShare = doc.abilities.accesses_manage;
const { toast } = useToastProvider();
const copyDocLink = useCopyDocLink(doc.id);
const { t } = useTranslation();
return (
<Box
@@ -27,12 +22,10 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
`}
>
<HorizontalSeparator $withPadding={true} />
{canShare && (
<>
<DocVisibility doc={doc} />
<HorizontalSeparator />
</>
)}
<DocVisibility doc={doc} />
<HorizontalSeparator />
<Box
$direction="row"
$justify="space-between"
@@ -41,18 +34,7 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
<Button
fullWidth={false}
onClick={() => {
navigator.clipboard
.writeText(window.location.href)
.then(() => {
toast(t('Link Copied !'), VariantType.SUCCESS, {
duration: 3000,
});
})
.catch(() => {
toast(t('Failed to copy link'), VariantType.ERROR, {
duration: 3000,
});
});
copyDocLink();
}}
color="tertiary"
icon={<span className="material-icons">add_link</span>}
@@ -26,10 +26,10 @@ export const DocShareModalInviteUserRow = ({ user }: Props) => {
color: var(--c--theme--colors--greyscale-400);
`}
>
<Text $theme="primary" $variation="600">
<Text $theme="primary" $variation="800">
{t('Add')}
</Text>
<Icon $theme="primary" $variation="600" iconName="add" />
<Icon $theme="primary" $variation="800" iconName="add" />
</Box>
}
/>
@@ -34,6 +34,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
const colors = colorsTokens();
const canManage = doc.abilities.accesses_manage;
const [linkReach, setLinkReach] = useState<LinkReach>(doc.link_reach);
const [docLinkRole, setDocLinkRole] = useState<LinkRole>(doc.link_role);
const { linkModeTranslations, linkReachChoices, linkReachTranslations } =
@@ -106,29 +107,35 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
$direction="row"
$align={isDesktop ? 'center' : undefined}
$padding={{ horizontal: '2xs' }}
$gap={spacing['3xs']}
$gap={canManage ? spacing['3xs'] : spacing['base']}
>
<DropdownMenu
label={t('Visibility')}
arrowCss={css`
color: ${colors['primary-800']} !important;
`}
disabled={!canManage}
showArrow={true}
options={linkReachOptions}
>
<Box $direction="row" $align="center" $gap={spacing['3xs']}>
<Icon
$theme="primary"
$variation="800"
$theme={canManage ? 'primary' : 'greyscale'}
$variation={canManage ? '800' : '600'}
iconName={linkReachChoices[linkReach].icon}
/>
<Text $theme="primary" $variation="800">
<Text
$theme={canManage ? 'primary' : 'greyscale'}
$variation={canManage ? '800' : '600'}
$weight="500"
$size="md"
>
{linkReachChoices[linkReach].label}
</Text>
</Box>
</DropdownMenu>
{isDesktop && (
<Text $size="xs" $variation="600">
<Text $size="xs" $variation="600" $weight="400">
{description}
</Text>
)}
@@ -137,6 +144,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
<Box $direction="row" $align="center" $gap={spacing['3xs']}>
{linkReach !== LinkReach.RESTRICTED && (
<DropdownMenu
disabled={!canManage}
showArrow={true}
options={linkMode}
label={t('Visibility mode')}
@@ -26,13 +26,13 @@ export const useTranslatedShareSettings = () => {
},
[LinkReach.AUTHENTICATED]: {
label: linkReachTranslations[LinkReach.AUTHENTICATED],
icon: 'corporate_fare',
icon: 'vpn_lock',
value: LinkReach.AUTHENTICATED,
descriptionReadOnly: t(
'Anyone with the link can see the document provided they are logged in',
'Anyone with the link can view the document if they are logged in',
),
descriptionEdit: t(
'Anyone with the link can edit provided they are logged in',
'Anyone with the link can edit the document if they are logged in',
),
},
[LinkReach.PUBLIC]: {
@@ -8,7 +8,7 @@ import { useResponsiveStore } from '@/stores';
const leftPaddingMap: { [key: number]: string } = {
3: '1.5rem',
2: '0.9rem',
1: '0.3',
1: '0.3rem',
};
export type HeadingsHighlight = {
@@ -10,6 +10,8 @@ import {
} from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
import { DocsGridItem } from './DocsGridItem';
import { DocsGridLoader } from './DocsGridLoader';
@@ -22,6 +24,7 @@ export const DocsGrid = ({
const { t } = useTranslation();
const { isDesktop } = useResponsiveStore();
const { flexLeft, flexRight } = useResponsiveDocGrid();
const {
data,
@@ -89,7 +92,7 @@ export const DocsGrid = ({
{title}
</Text>
{!hasDocs && (
{!hasDocs && !loading && (
<Box $padding={{ vertical: 'sm' }} $align="center" $justify="center">
<Text $size="sm" $variation="600" $weight="700">
{t('No documents found')}
@@ -101,23 +104,21 @@ export const DocsGrid = ({
<Box
$direction="row"
$padding={{ horizontal: 'xs' }}
$gap="20px"
$gap="10px"
data-testid="docs-grid-header"
>
<Box $flex={6} $padding="3xs">
<Box $flex={flexLeft} $padding="3xs">
<Text $size="xs" $variation="600" $weight="500">
{t('Name')}
</Text>
</Box>
{isDesktop && (
<Box $flex={2} $padding="3xs">
<Box $flex={flexRight} $padding={{ vertical: '3xs' }}>
<Text $size="xs" $weight="500" $variation="600">
{t('Updated at')}
</Text>
</Box>
)}
<Box $flex={1.15} $align="flex-end" $padding="3xs" />
</Box>
{/* Body */}
@@ -20,6 +20,7 @@ export const DocsGridActions = ({
openShareModal,
}: DocsGridActionsProps) => {
const { t } = useTranslation();
const deleteModal = useModal();
const removeFavoriteDoc = useDeleteFavoriteDoc({
@@ -45,7 +46,10 @@ export const DocsGridActions = ({
{
label: t('Share'),
icon: 'group',
callback: () => openShareModal?.(),
callback: () => {
openShareModal?.();
},
testId: `docs-grid-actions-share-${doc.id}`,
},
@@ -1,27 +1,33 @@
import { Button, useModal } from '@openfun/cunningham-react';
import { Tooltip, useModal } from '@openfun/cunningham-react';
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, LinkReach } from '@/features/docs/doc-management';
import { DocShareModal } from '@/features/docs/doc-share';
import { useResponsiveStore } from '@/stores';
import { DocsGridActions } from './DocsGridActions';
import { SimpleDocItem } from './SimpleDocItem';
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
import { DocsGridActions } from './DocsGridActions';
import { DocsGridItemSharedButton } from './DocsGridItemSharedButton';
import { SimpleDocItem } from './SimpleDocItem';
type DocsGridItemProps = {
doc: Doc;
};
export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
const { t } = useTranslation();
const { isDesktop } = useResponsiveStore();
const { flexLeft, flexRight } = useResponsiveDocGrid();
const { spacingsTokens } = useCunninghamTheme();
const spacings = spacingsTokens();
const shareModal = useModal();
const isPublic = doc.link_reach === LinkReach.PUBLIC;
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
const isRestricted = doc.link_reach === LinkReach.RESTRICTED;
const sharedCount = doc.nb_accesses - 1;
const isShared = sharedCount > 0;
const showAccesses = isPublic || isAuthenticated;
const handleShareClick = () => {
shareModal.open();
@@ -33,8 +39,8 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
$direction="row"
$width="100%"
$align="center"
$gap="20px"
role="row"
$gap="20px"
$padding={{ vertical: '2xs', horizontal: isDesktop ? 'base' : 'xs' }}
$css={css`
cursor: pointer;
@@ -45,75 +51,76 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
`}
>
<StyledLink
$css="flex: 8; align-items: center;"
$css={css`
flex: ${flexLeft};
align-items: center;
`}
href={`/docs/${doc.id}`}
>
<Box
data-testid={`docs-grid-name-${doc.id}`}
$flex={6}
$padding={{ right: 'base' }}
$direction="row"
$align="center"
$gap={spacings.xs}
$flex={flexLeft}
$padding={{ right: isDesktop ? 'md' : '3xs' }}
>
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
{showAccesses && (
<Box
$padding={{ top: '3xs' }}
$css={css`
align-self: flex-start;
`}
>
<Tooltip
content={
<Text $textAlign="center" $variation="000">
{isPublic
? t('Accessible to anyone')
: t('Accessible to authenticated users')}
</Text>
}
placement="top"
>
<div>
<Icon
$theme="greyscale"
$variation="600"
$size="14px"
iconName={isPublic ? 'public' : 'vpn_lock'}
/>
</div>
</Tooltip>
</Box>
)}
</Box>
</StyledLink>
<Box
$flex={flexRight}
$direction="row"
$align="center"
$justify={isDesktop ? 'space-between' : 'flex-end'}
$gap="32px"
>
{isDesktop && (
<Box $flex={2}>
<StyledLink href={`/docs/${doc.id}`}>
<Text $variation="600" $size="xs">
{DateTime.fromISO(doc.updated_at).toRelative()}
</Text>
</Box>
</StyledLink>
)}
</StyledLink>
<Box
$flex={1.15}
$direction="row"
$align="center"
$justify="flex-end"
$gap="32px"
>
{isDesktop && isPublic && (
<Button
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleShareClick();
}}
size="nano"
fullWidth
icon={<Icon $variation="000" iconName="public" />}
>
{isShared ? sharedCount : undefined}
</Button>
)}
{isDesktop && !isPublic && isRestricted && isShared && (
<Button
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleShareClick();
}}
fullWidth
color="tertiary"
size="nano"
icon={<Icon $variation="800" $theme="primary" iconName="group" />}
>
{sharedCount}
</Button>
)}
{isDesktop && !isPublic && isAuthenticated && (
<Button
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleShareClick();
}}
fullWidth
size="nano"
icon={<Icon $variation="000" iconName="corporate_fare" />}
>
{sharedCount}
</Button>
)}
<DocsGridActions doc={doc} openShareModal={handleShareClick} />
<Box $direction="row" $align="center" $gap={spacings.lg}>
{isDesktop && (
<DocsGridItemSharedButton
doc={doc}
handleClick={handleShareClick}
/>
)}
<DocsGridActions doc={doc} openShareModal={handleShareClick} />
</Box>
</Box>
</Box>
{shareModal.isOpen && (
@@ -0,0 +1,45 @@
import { Button, Tooltip } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box, Icon, Text } from '@/components';
import { Doc } from '../../doc-management';
type Props = {
doc: Doc;
handleClick: () => void;
};
export const DocsGridItemSharedButton = ({ doc, handleClick }: Props) => {
const { t } = useTranslation();
const sharedCount = doc.nb_accesses;
const isShared = sharedCount - 1 > 0;
if (!isShared) {
return <Box $minWidth="50px">&nbsp;</Box>;
}
return (
<Tooltip
content={
<Text $textAlign="center" $variation="000">
{t('Shared with {{count}} users', { count: sharedCount })}
</Text>
}
placement="top"
>
<Button
style={{ minWidth: '50px', justifyContent: 'center' }}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleClick();
}}
color="tertiary"
size="nano"
icon={<Icon $variation="800" $theme="primary" iconName="group" />}
>
{sharedCount}
</Button>
</Tooltip>
);
};
@@ -1,9 +1,9 @@
import { DateTime } from 'luxon';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, LinkReach } from '@/features/docs/doc-management';
import { Doc } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import PinnedDocumentIcon from '../assets/pinned-document.svg';
@@ -34,11 +34,6 @@ export const SimpleDocItem = ({
const { isDesktop } = useResponsiveStore();
const spacings = spacingsTokens();
const isPublic = doc?.link_reach === LinkReach.PUBLIC;
const isShared = !isPublic && doc.nb_accesses > 1;
const accessCount = doc.nb_accesses - 1;
const isSharedOrPublic = isShared || isPublic;
return (
<Box $direction="row" $gap={spacings.sm}>
<Box
@@ -69,22 +64,6 @@ export const SimpleDocItem = ({
$gap={spacings['3xs']}
$margin={{ top: '-2px' }}
>
{isPublic && (
<Icon iconName="public" $size="16px" $variation="600" />
)}
{isShared && (
<Icon iconName="group" $size="16px" $variation="600" />
)}
{isSharedOrPublic && accessCount > 0 && (
<Text $size="12px" $weight="bold" $variation="600">
{accessCount}
</Text>
)}
{isSharedOrPublic && (
<Text $size="12px" $variation="600">
·
</Text>
)}
<Text $variation="600" $size="xs">
{DateTime.fromISO(doc.updated_at).toRelative()}
</Text>
@@ -0,0 +1,29 @@
import { useMemo } from 'react';
import { useResponsiveStore } from '@/stores';
export const useResponsiveDocGrid = () => {
const { isDesktop, screenWidth } = useResponsiveStore();
const flexLeft = useMemo(() => {
if (!isDesktop) {
return 1;
} else if (screenWidth <= 1100) {
return 6;
} else if (screenWidth < 1200) {
return 8;
}
return 8;
}, [isDesktop, screenWidth]);
const flexRight = useMemo(() => {
if (!isDesktop) {
return undefined;
} else if (screenWidth <= 1200) {
return 5;
}
return 4;
}, [isDesktop, screenWidth]);
return { flexLeft, flexRight };
};
@@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation';
import { PropsWithChildren } from 'react';
import { Box, Icon, SeparatedSection } from '@/components';
import { useAuthStore } from '@/core';
import { useCreateDoc } from '@/features/docs/doc-management';
import { DocSearchModal } from '@/features/docs/doc-search';
import { useCmdK } from '@/hook/useCmdK';
@@ -13,6 +14,7 @@ import { useLeftPanelStore } from '../stores';
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
const router = useRouter();
const searchModal = useModal();
const auth = useAuthStore();
useCmdK(searchModal.open);
const { togglePanel } = useLeftPanelStore();
@@ -52,16 +54,20 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
<Icon $variation="800" $theme="primary" iconName="house" />
}
/>
<Button
onClick={searchModal.open}
size="medium"
color="tertiary-text"
icon={
<Icon $variation="800" $theme="primary" iconName="search" />
}
/>
{auth.authenticated && (
<Button
onClick={searchModal.open}
size="medium"
color="tertiary-text"
icon={
<Icon $variation="800" $theme="primary" iconName="search" />
}
/>
)}
</Box>
<Button onClick={createNewDoc}>{t('New doc')}</Button>
{auth.authenticated && (
<Button onClick={createNewDoc}>{t('New doc')}</Button>
)}
</Box>
</SeparatedSection>
{children}
@@ -1 +1,2 @@
export * from './useDate';
export * from './useClipboard';
@@ -0,0 +1,34 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const useClipboard = () => {
const { toast } = useToastProvider();
const { t } = useTranslation();
return useCallback(
(text: string, successMessage?: string, errorMessage?: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
toast(
successMessage ?? t('Copied to clipboard'),
VariantType.SUCCESS,
{
duration: 3000,
},
);
})
.catch(() => {
toast(
errorMessage ?? t('Failed to copy to clipboard'),
VariantType.ERROR,
{
duration: 3000,
},
);
});
},
[t, toast],
);
};
@@ -2,85 +2,145 @@
"de": {
"translation": {
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" ist bereits zum Dokument eingeladen.",
"\"{{email}}\" is already member of the document.": "\"{{email}}\" ist bereits Mitglied des Dokuments.",
"AI Actions": "KI-Aktionen",
"AI seems busy! Please try again.": "KI scheint beschäftigt! Bitte versuchen Sie es erneut.",
"Accessibility": "Barrierefreiheit",
"Accessibility statement": "Erklärung zur Barrierefreiheit",
"Add": "Hinzufügen",
"Address:": "Anschrift:",
"Administrator": "Administrator",
"Anyone on the internet with the link can view": "Für jeden im Internet mit diesem Link sichtbar",
"All docs": "Alle Dokumente",
"Anonymous": "Gast",
"Anyone with the link can edit the document": "Jeder mit dem Link kann das Dokument bearbeiten",
"Anyone with the link can edit the document if they are logged in": "Jeder mit dem Link kann das Dokument bearbeiten, wenn er angemeldet ist",
"Anyone with the link can see the document": "Jeder mit dem Link kann das Dokument ansehen",
"Anyone with the link can view the document if they are logged in": "Jeder mit dem Link kann das Dokument ansehen, wenn er angemeldet ist",
"Are you sure you want to delete the document \"{{title}}\"?": "Sind Sie sicher, dass Sie das Dokument \"{{title}}\" löschen möchten?",
"Back to home page": "Zurück zur Startseite",
"Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.",
"Cancel": "Abbrechen",
"Choose a role": "Wählen Sie eine Rolle",
"Close the modal": "Pop up schliessen",
"Compliance status": "Konformitätsstatus",
"Confirm deletion": "Löschung bestätigen",
"Connected": "Verbunden",
"Content modal to delete document": "Inhalts-Modal zum Löschen des Dokuments",
"Content modal to export the document": "Inhalte zum Exportieren des Dokuments",
"Convert Markdown": "Markdown konvertieren",
"Copied to clipboard": "In die Zwischenablage kopiert",
"Copy as {{format}}": "Als {{format}} kopieren",
"Copy link": "Link kopieren",
"Correct": "Korrigieren",
"Delete": "Löschen",
"Delete a doc": "Dokument löschen",
"Delete document": "Dokument löschen",
"Deleting the document \"{{title}}\"": "Lösche das Dokument \"{{title}}\"",
"Doc visibility card": "Dokumenten-Sichtbarkeitskarte",
"Docs": "Docs",
"Docs Logo": "Docs Logo",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Pages: Ihr neuer Begleiter für eine effiziente, intuitive und sichere Zusammenarbeit bei Dokumenten.",
"Document owner": "Besitzer des Dokuments",
"Document title updated successfully": "Titel des Dokuments erfolgreich aktualisiert",
"Download": "Herunterladen",
"E-mail:": "E-Mail:",
"Edition": "Bearbeiten",
"Editor": "Editor",
"Editor unavailable": "Editor nicht verfügbar",
"Error during delete invitation": "Fehler beim Löschen der Einladung",
"Error during invitation update": "Fehler beim Aktualisieren der Einladung",
"Error during update invitation": "Fehler beim Aktualisieren der Einladung",
"Error while deleting invitation": "Fehler beim Löschen der Einladung",
"Export": "Exportieren",
"Failed to add the member in the document.": "Fehler beim Hinzufügen des Mitglieds zum Dokument.",
"Failed to copy link": "Link konnte nicht kopiert werden",
"Failed to copy to clipboard": "Fehler beim Kopieren in die Zwischenablage",
"Failed to create the invitation for {{email}}.": "Fehler beim Erstellen der Einladung für {{email}}.",
"Find a member to add to the document": "Suchen Sie ein Mitglied, das dem Dokument hinzugefügt werden soll",
"Format": "Format",
"History": "Versionsverlauf",
"If a member is editing, his works can be lost.": "Wenn ein Mitglied editiert, können seine Änderungen verloren gehen.",
"Improvement and contact": "Verbesserungen und Kontakt",
"Invitation sent to {{email}}.": "Einladung an {{email}} gesendet.",
"Invite new members to {{title}}": "Neue Mitglieder zu {{title}} einladen",
"Invited": "Eingeladen",
"Invite": "Einladen",
"It is the card information about the document.": "Es handelt sich um die Karteninformationen zum Dokument.",
"It is the document title": "Es ist der Titel des Dokuments",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Es scheint, dass die von Ihnen gesuchte Seite nicht existiert oder nicht korrekt angezeigt werden kann.",
"Language": "Sprache",
"Last update: {{update}}": "Zuletzt aktualisiert: {{update}}",
"Legal Notice": "Impressum",
"Legal notice": "Impressum",
"Link Copied !": "Link kopiert!",
"Link parameters": "Link-Parameter",
"List invitation card": "Einladungsliste anzeigen",
"List members card": "Mitgliederliste anzeigen",
"Load more": "Mehr anzeigen",
"Login": "Anmelden",
"Logout": "Abmelden",
"Modal confirmation to restore the version": "Modale Bestätigung um die Version wiederherzustellen",
"More docs": "Weitere Dokumente",
"My docs": "Meine Dokumente",
"Name": "Name",
"New doc": "Neues Dokument",
"No active search": "Keine aktive Suche",
"No document found": "Kein Dokument gefunden",
"No documents found": "Keine Dokumente gefunden",
"No editor found": "Kein Editor gefunden",
"No versions": "Keine Versionen",
"OK": "OK",
"Offline ?!": "Offline?!",
"Only for people with access": "Nur für Personen mit Zugriff",
"Only invited people can access": "Nur eingeladene Personen haben Zugriff",
"Open the document options": "Öffnen Sie die Dokumentoptionen",
"Open the header menu": "Öffne das Kopfzeilen-Menü",
"Ouch !": "Autsch!",
"Owner": "Besitzer",
"PDF": "PDF",
"Pending invitations": "Ausstehende Einladungen",
"Personal data and cookies": "Personenbezogene Daten und Cookies",
"Pin": "Anheften",
"Pinned documents": "Angepinnte Dokumente",
"Private": "Privat",
"Public": "Öffentlich",
"Read only, you cannot edit this document.": "Nur lesen: Sie können dieses Dokument nicht bearbeiten.",
"Public document": "Öffentliches Dokument",
"Quick search input": "Schnellsuche-Eingabe",
"Reader": "Leser",
"Reading": "Lesen",
"Remove": "Löschen",
"Rename": "Umbenennen",
"Rephrase": "Umformulieren",
"Restore": "Wiederherstellen",
"Role": "Rolle",
"Search by email": "Nach E-Mail suchen",
"Search": "Suchen",
"Search modal": "Suche Modal",
"Search user result": "Suchergebnis",
"Select a document": "Dokument auswählen",
"Select a version on the right to restore": "Wählen Sie rechts eine Version zum Wiederherstellen aus",
"Share": "Teilen",
"Share modal": "Teilen-Modal",
"Share the document": "Dokument teilen",
"Share with {{count}} users_many": "Teilen mit {{count}} Benutzern",
"Share with {{count}} users_one": "Teilen mit {{count}} Benutzern",
"Share with {{count}} users_other": "Teilen mit {{count}} Benutzern",
"Shared with me": "Mit mir geteilt",
"Something bad happens, please retry.": "Etwas ist schiefgelaufen, bitte versuchen Sie es erneut.",
"Table of contents": "Inhaltsverzeichnis",
"Summarize": "Zusammenfassen",
"Summary": "Zusammenfassung",
"Template": "Vorlage",
"The document has been deleted.": "Das Dokument wurde gelöscht.",
"The invitation has been removed.": "Die Einladung wurde zurückgenommen.",
"The member has been removed from the document": "Das Mitglied wurde aus dem Dokument entfernt",
"The role has been updated": "Die Rolle wurde aktualisiert",
"The role has been updated.": "Die Rolle wurde aktualisiert.",
"The document visibility has been updated.": "Die Sichtbarkeit des Dokuments wurde aktualisiert.",
"This accessibility statement applies to the site hosted on": "Diese Erklärung zur Barrierefreiheit gilt für die gehostete Seite",
"This site does not display a cookie consent banner, why?": "",
"Too many requests. Please wait 60 seconds.": "Zu viele Anfragen. Bitte warten Sie 60 Sekunden.",
"Type a name or email": "Geben Sie einen Namen oder eine E-Mail-Adresse ein",
"Type the name of a document": "Geben Sie den Namen eines Dokuments ein",
"Unless otherwise stated, all content on this site is under": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
"Unpin": "Lösen",
"Untitled document": "Unbenanntes Dokument",
"Updated at": "Aktualisiert am",
"User {{email}} added to the document.": "Benutzer {{email}} wurde dem Dokument hinzugefügt.",
"Validate": "Bestätigen",
"Upload your docs to a Microsoft Word, Open Office or PDF document.": "Laden Sie Ihre Dokumente zu einem Microsoft Word, Open Office oder PDF Dokument hoch.",
"Use as prompt": "Als Prompt verwenden",
"Version history": "Versionsverlauf",
"Version restored successfully": "Version erfolgreich wiederhergestellt",
"We didn't find a mail matching, try to be more accurate": "Wir haben keine übereinstimmende E-Mail gefunden, versuchen Sie genauer zu sein",
"Visibility": "Sichtbarkeit",
"Visibility mode": "Sichtbarkeitseinstellungen",
"Warning": "Warnung",
"We try to respond within 2 working days.": "Wir versuchen, innerhalb von 2 Arbeitstagen zu antworten.",
"Word / Open Office": "Word / Open Office",
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Sie sind der einzige Besitzer dieser Gruppe. Machen Sie ein anderes Mitglied zum Gruppenbesitzer, bevor Sie Ihre eigene Rolle ändern oder aus Ihrem Dokument entfernen können.",
"You cannot update the role or remove other owner.": "Sie können die Rolle nicht aktualisieren oder einen anderen Besitzer entfernen.",
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
@@ -96,21 +156,23 @@
"AI seems busy! Please try again.": "L'IA semble occupée ! Veuillez réessayer.",
"Accessibility": "Accessibilité",
"Accessibility statement": "Déclaration d'accessibilité",
"Add": "Ajouter",
"Address:": "Adresse :",
"Administrator": "Administrateur",
"All docs": "Tous les documents",
"Anonymous": "Anonyme",
"Anyone on the internet with the link can view": "Les personnes disposant du lien peuvent y accéder",
"Anyone with the link can edit the document": "N'importe qui avec le lien peut éditer le document",
"Anyone with the link can edit the document if they are logged in": "N'importe qui avec le lien peut éditer le document à condition qu'il soit connecté",
"Anyone with the link can see the document": "N'importe qui avec le lien peut voir le document",
"Anyone with the link can view the document if they are logged in": "N'importe qui avec le lien peut voir le document à condition qu'il soit connecté",
"Are you sure you want to delete the document \"{{title}}\"?": "Êtes-vous sûr de vouloir supprimer le document \"{{title}}\" ?",
"Authenticated": "Authentifié",
"Back to home page": "Retour à l'accueil",
"Can read and edit": "Peut lire et éditer",
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
"Cancel": "Annuler",
"Choose a role": "Choisissez un rôle",
"Close the modal": "Fermer la modale",
"Compliance status": "État de conformité",
"Confirm deletion": "Confirmer la suppression",
"Connected": "Connecté",
"Content modal to delete document": "Contenu modal pour supprimer le document",
"Content modal to export the document": "Contenu modal pour exporter le document",
"Convert Markdown": "Convertir le Markdown",
@@ -121,35 +183,38 @@
"Copyright": "Copyright",
"Correct": "Corriger",
"Defender of Rights - Free response - 71120 75342 Paris CEDEX 07": "Défenseur des droits - Réponse gratuite - 71120 75342 Paris CEDEX 07",
"Delete": "Supprimer",
"Delete a doc": "Supprimer un doc",
"Delete document": "Supprimer le document",
"Deleting the document \"{{title}}\"": "Suppression du document \"{{title}}\"",
"Doc visibility card": "Carte de visibilité du doc",
"Docs": "Docs",
"Docs Logo": "Logo Docs",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs : Votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement et en toute sécurité.",
"Document owner": "Propriétaire du document",
"Document title updated successfully": "Titre du document mis à jour avec succès",
"Download": "Télécharger",
"E-mail:": "E-mail:",
"Edition": "Édition",
"Editor": "Éditeur",
"Editor unavailable": "Éditeur indisponible",
"Error during delete invitation": "Erreur lors de la suppression de l'invitation",
"Error during invitation update": "Erreur lors de la mise à jour de l'invitation",
"Error during update invitation": "Erreur lors de la mise à jour de l'invitation",
"Error while deleting invitation": "Erreur lors de la suppression de l'invitation",
"Established on December 20, 2023.": "Établi le 20 décembre 2023.",
"Export": "Exporter",
"Failed to add the member in the document.": "Impossible d'ajouter le membre dans le document.",
"Failed to copy link": "Échec de la copie du lien",
"Failed to copy to clipboard": "Échec de la copie dans le presse-papier",
"Failed to create the invitation for {{email}}.": "Impossible de créer l'invitation pour {{email}}.",
"Find a member to add to the document": "Trouver un membre à ajouter au document",
"Format": "Format",
"French Interministerial Directorate for Digital Affairs (DINUM), 20 avenue de Ségur 75007 Paris.": "Direction interministérielle des affaires numériques (DINUM), 20 avenue de Segur 75007 Paris.",
"History": "Historique",
"How people can interact with the document": "Comment les gens peuvent interagir avec le document",
"If a member is editing, his works can be lost.": "Si un membre est en train d'éditer, ses travaux peuvent être perdus.",
"If you are unable to access a content or a service, you can contact the person responsible for https://lasuite.numerique.gouv.fr to be directed to an accessible alternative or to obtain the content in another form.": "Si vous ne pouvez pas accéder à un contenu ou à un service, vous pouvez contacter la personne responsable de https://lasuite. umerique.gouv.fr pour être dirigé vers une alternative accessible ou pour obtenir le contenu sous une autre forme.",
"Illustration:": "Illustration :",
"Improvement and contact": "Amélioration et contact",
"Invitation sent to {{email}}.": "Invitation envoyée à {{email}}.",
"Invite new members to {{title}}": "Invitez de nouveaux membres à rejoindre {{title}}",
"Invited": "Invité",
"Invite": "Inviter",
"It is the card information about the document.": "Il s'agit de la carte d'information du document.",
"It is the document title": "Il s'agit du titre du document",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
@@ -159,6 +224,7 @@
"Legal Notice": "Mentions Legales",
"Legal notice": "Mention légale",
"Link Copied !": "Lien copié !",
"Link parameters": "Paramètres du lien",
"List invitation card": "Carte de liste d'invitation",
"List members card": "Carte liste des membres",
"Load more": "Afficher plus",
@@ -170,50 +236,57 @@
"My docs": "Mes documents",
"Name": "Nom",
"New doc": "Nouveau doc",
"No active search": "Aucune recherche active",
"No document found": "Aucun document trouvé",
"No documents found": "Aucun document trouvé",
"No editor found": "Pas d'éditeur trouvé",
"No versions": "Aucune version",
"Nothing exceptional, no special privileges related to a .gouv.fr.": "Rien d'exceptionnel, pas de privilèges spéciaux liés à un .gouv.fr.",
"OK": "OK",
"Offline ?!": "Hors-ligne ?!",
"Only for authenticated users": "Uniquement pour les utilisateurs authentifiés",
"Only for people with access": "Seulement pour les personnes avec accès",
"Only invited people can access": "Seules les personnes invitées peuvent accéder",
"Open the document options": "Ouvrir les options du document",
"Open the header menu": "Ouvrir le menu d'en-tête",
"Ouch !": "Aïe !",
"Owner": "Propriétaire",
"PDF": "PDF",
"Pending invitations": "Invitations en attente",
"Personal data and cookies": "Données personnelles et cookies",
"Pin": "Épingler",
"Pinned documents": "Documents épinglés",
"Private": "Privé",
"Public": "Public",
"Public document": "Document public",
"Publication Director": "Directeur de la publication",
"Publisher": "Éditeur",
"Read only": "Lecture seule",
"Read only, you cannot edit this document.": "En lecture seule, vous ne pouvez pas éditer ce document.",
"Quick search input": "Saisie de recherche rapide",
"Reader": "Lecteur",
"Reading": "Lecture seul",
"Remedies": "Voie de recours",
"Remove": "Supprimer",
"Rename": "Renommer",
"Rephrase": "Reformuler",
"Restore": "Restaurer",
"Restricted": "Restreint",
"Role": "Rôle",
"Search by email": "Recherche par email",
"Search": "Rechercher",
"Search modal": "Modale de partage",
"Search user result": "Résultat de la recherche utilisateur",
"Select a document": "Sélectionnez un document",
"Select a version on the right to restore": "Sélectionnez une version à droite à restaurer",
"Send a letter by post (free of charge, no stamp needed):": "Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre):",
"Share": "Partager",
"Share modal": "Modale de partage",
"Share the document": "Partager le document",
"Share with {{count}} users_many": "Partager avec {{count}} utilisateurs",
"Share with {{count}} users_one": "Partager avec {{count}} utilisateur",
"Share with {{count}} users_other": "Partager avec {{count}} utilisateurs",
"Shared with me": "Partagés avec moi",
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
"Summarize": "Résumer",
"Summary": "Sommaire",
"Table of contents": "Table des matières",
"Template": "Template",
"The document has been deleted.": "Le document a bien été supprimé.",
"The document visibility has been updated.": "La visibilité du document a été mise à jour.",
"The invitation has been removed.": "L'invitation a été supprimée.",
"The member has been removed from the document": "Le membre a été retiré du document",
"The role has been updated": "Le rôle a été mis à jour",
"The role has been updated.": "Le rôle a été mis à jour.",
"The team in charge of the digital workspace \"La Suite numérique\" can be contacted directly at": "L'équipe responsable de l'espace de travail numérique \"La Suite numérique\" peut être contactée directement à l'adresse",
"This accessibility statement applies to the site hosted on": "Cette déclaration d'accessibilité s'applique au site hébergé sur",
"This allows us to measure the number of visits and understand which pages are the most viewed.": "Cela nous permet de mesurer le nombre de visites et de comprendre quelles pages sont les plus consultées.",
@@ -222,18 +295,19 @@
"This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.",
"This will protect your privacy, but will also prevent the owner from learning from your actions and creating a better experience for you and other users.": "Cela protégera votre vie privée, mais empêchera également le propriétaire d'apprendre de vos actions et de créer une meilleure expérience pour vous et les autres utilisateurs.",
"Too many requests. Please wait 60 seconds.": "Trop de demandes. Veuillez patienter 60 secondes.",
"Type a name or email": "Tapez un nom ou un email",
"Type the name of a document": "Tapez le nom d'un document",
"Unless otherwise stated, all content on this site is under": "Sauf mention contraire, tout le contenu de ce site est sous",
"Unpin": "Désépingler",
"Untitled document": "Document sans titre",
"Updated at": "Mise à jour le",
"Upload your docs to a Microsoft Word, Open Office or PDF document.": "Téléchargez vos documents dans un document Microsoft Word, Open Office ou PDF.",
"Use as prompt": "Utiliser comme un prompt",
"User {{email}} added to the document.": "L'utilisateur {{email}} a été ajouté au document.",
"Validate": "Valider",
"Version history": "Historique des versions",
"Version restored successfully": "Version restaurée avec succès",
"Visibility": "Visibilité",
"Visibility mode": "Mode de visibilité",
"Warning": "Attention",
"We didn't find a mail matching, try to be more accurate": "Nous n'avons pas trouvé de correspondance par mail, essayez d'être plus précis",
"We simply comply with the law, which states that certain audience measurement tools, properly configured to respect privacy, are exempt from prior authorization.": "Nous nous conformons simplement à la loi, qui stipule que certains outils de mesure daudience, correctement configurés pour respecter la vie privée, sont exemptés de toute autorisation préalable.",
"We try to respond within 2 working days.": "Nous essayons de répondre dans les 2 jours ouvrables.",
"Word / Open Office": "Word / Open Office",
@@ -0,0 +1,46 @@
import { Router } from 'next/router';
import posthog from 'posthog-js';
import { PostHogProvider as PHProvider } from 'posthog-js/react';
import { PropsWithChildren, useEffect } from 'react';
export interface PostHogConf {
id: string;
host: string;
}
interface PostHogProviderProps {
conf?: PostHogConf;
}
export function PostHogProvider({
children,
conf,
}: PropsWithChildren<PostHogProviderProps>) {
useEffect(() => {
if (!conf?.id || !conf?.host || posthog.__loaded) {
return;
}
posthog.init(conf.id, {
api_host: conf.host,
person_profiles: 'always',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') {
posthog.debug();
}
},
capture_pageview: false,
capture_pageleave: true,
});
const handleRouteChange = () => posthog?.capture('$pageview');
Router.events.on('routeChangeComplete', handleRouteChange);
return () => {
Router.events.off('routeChangeComplete', handleRouteChange);
};
}, [conf?.host, conf?.id]);
return <PHProvider client={posthog}>{children}</PHProvider>;
}
@@ -1 +1,2 @@
export * from './Crisp';
export * from './Posthog';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "1.10.0",
"version": "2.0.1",
"private": true,
"workspaces": {
"packages": [
@@ -1,6 +1,6 @@
{
"name": "eslint-config-impress",
"version": "1.10.0",
"version": "2.0.1",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "1.10.0",
"version": "2.0.1",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "1.10.0",
"version": "2.0.1",
"description": "Y.js provider for docs",
"repository": "https://github.com/numerique-gouv/impress",
"license": "MIT",
@@ -35,8 +35,8 @@
"@types/node": "*",
"@types/supertest": "6.0.2",
"@types/ws": "8.5.13",
"eslint-config-impress": "*",
"cross-env": "7.0.3",
"eslint-config-impress": "*",
"jest": "29.7.0",
"nodemon": "3.1.9",
"supertest": "7.0.0",
+96 -10
View File
@@ -988,7 +988,56 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@blocknote/core@0.22.0", "@blocknote/core@^0.22.0":
"@blocknote/core@0.21.0", "@blocknote/core@^0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.21.0.tgz#b54baaa3eca3b700c80c59113a837c3d4153dca2"
integrity sha512-TQAN0qRCkXpz5AwfdxjuFvIKWsU4bBI5d/e5iX7PEkhwf4PFgPHsMUl2uuGw5o/hhjzoU54YKaAjlJlWyw4goA==
dependencies:
"@emoji-mart/data" "^1.2.1"
"@tiptap/core" "^2.7.1"
"@tiptap/extension-bold" "^2.7.1"
"@tiptap/extension-code" "^2.7.1"
"@tiptap/extension-collaboration" "^2.7.1"
"@tiptap/extension-collaboration-cursor" "^2.7.1"
"@tiptap/extension-gapcursor" "^2.7.1"
"@tiptap/extension-hard-break" "^2.7.1"
"@tiptap/extension-history" "^2.7.1"
"@tiptap/extension-horizontal-rule" "^2.7.1"
"@tiptap/extension-italic" "^2.7.1"
"@tiptap/extension-link" "^2.7.1"
"@tiptap/extension-paragraph" "^2.7.1"
"@tiptap/extension-strike" "^2.7.1"
"@tiptap/extension-table-cell" "^2.7.1"
"@tiptap/extension-table-header" "^2.7.1"
"@tiptap/extension-table-row" "^2.7.1"
"@tiptap/extension-text" "^2.7.1"
"@tiptap/extension-underline" "^2.7.1"
"@tiptap/pm" "^2.7.1"
emoji-mart "^5.6.0"
hast-util-from-dom "^4.2.0"
prosemirror-dropcursor "^1.8.1"
prosemirror-highlight "^0.9.0"
prosemirror-model "^1.23.0"
prosemirror-state "^1.4.3"
prosemirror-tables "^1.6.1"
prosemirror-transform "^1.9.0"
prosemirror-view "^1.33.7"
rehype-format "^5.0.0"
rehype-parse "^8.0.4"
rehype-remark "^9.1.2"
rehype-stringify "^9.0.3"
remark-gfm "^3.0.1"
remark-parse "^10.0.1"
remark-rehype "^10.1.0"
remark-stringify "^10.0.2"
shiki "^1.22.0"
unified "^10.1.2"
uuid "^8.3.2"
y-prosemirror "1.2.13"
y-protocols "^1.0.6"
yjs "^13.6.15"
"@blocknote/core@^0.22.0":
version "0.22.0"
resolved "https://registry.yarnpkg.com/@blocknote/core/-/core-0.22.0.tgz#2f363f9677d4fa5f20299b22850f5f34a6340a55"
integrity sha512-AAEx01zK6u+b1SsZniMm/aogEMjasF4vA9ZHgFGj04G7AwK5Hjwa0Sxre58qcW+KzuvR09CQHTkwjmgVmJX/HA==
@@ -1037,19 +1086,31 @@
y-protocols "^1.0.6"
yjs "^13.6.15"
"@blocknote/mantine@0.22.0":
version "0.22.0"
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.22.0.tgz#15509aaefe88c3efd73a884b9fb1e0584a6223ec"
integrity sha512-6irIKCGUpE47X8qWLx9oa5ndztSrvLEHgVRp+fdVUHMJCx0/OzijJyYTTFKw8yEI9qc01pjmwdYMZrMMZybyGw==
"@blocknote/mantine@0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@blocknote/mantine/-/mantine-0.21.0.tgz#b8a640f498a4884129fe33f854be8d2bb842ea41"
integrity sha512-GAxgvn/87wDyE8qdkystTkEbqE8AFO81gaMJ6df0P6ZAdfIH3sFYUf9MffVOjtq7T6NSCM9vHNnhHsC9K8m/fg==
dependencies:
"@blocknote/core" "^0.22.0"
"@blocknote/react" "^0.22.0"
"@blocknote/core" "^0.21.0"
"@blocknote/react" "^0.21.0"
"@mantine/core" "^7.10.1"
"@mantine/hooks" "^7.10.1"
"@mantine/utils" "^6.0.21"
react-icons "^5.2.1"
"@blocknote/react@0.22.0", "@blocknote/react@^0.22.0":
"@blocknote/react@0.21.0", "@blocknote/react@^0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.21.0.tgz#ad8907f89575e8c139d07d75bdb66ef4e33f84f9"
integrity sha512-eBKe3hihGNeO4G/qKKJ/B5uuEmWm8XMbT8SxJ2zpNTjHx5lLP45vhtjAM+HCzQqz4xYacc2NphUIdjPPH5eXrQ==
dependencies:
"@blocknote/core" "^0.21.0"
"@floating-ui/react" "^0.26.4"
"@tiptap/core" "^2.7.1"
"@tiptap/react" "^2.7.1"
lodash.merge "^4.6.2"
react-icons "^5.2.1"
"@blocknote/react@^0.22.0":
version "0.22.0"
resolved "https://registry.yarnpkg.com/@blocknote/react/-/react-0.22.0.tgz#a17167a26b70ef421218ae3e49d15cca751291f0"
integrity sha512-Y6Oj99iOKnlh2FE/lgy8kO5PziPnA8MyEJyjCH9Jbvlc9t493L9EFmLK8iKBZek7sh0TOzhXGBOA6lIpk02X6A==
@@ -6225,7 +6286,7 @@ core-js-compat@^3.38.0, core-js-compat@^3.38.1:
dependencies:
browserslist "^4.24.2"
core-js@^3.0.0:
core-js@^3.0.0, core-js@^3.38.1:
version "3.39.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83"
integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==
@@ -7475,6 +7536,11 @@ fetch-mock@9.11.0:
querystring "^0.2.0"
whatwg-url "^6.5.0"
fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
figlet@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.7.0.tgz#46903a04603fd19c3e380358418bb2703587a72e"
@@ -10860,6 +10926,21 @@ postgres-range@^1.1.1:
resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863"
integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==
posthog-js@1.204.0:
version "1.204.0"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.204.0.tgz#73843af471fcc484ca1e8e1bcc927887cf81b4ba"
integrity sha512-wVt948wKPPztCZ3OeDq8y0dtaPbhbY8vFuEVBUNHOn7PohbTXr7HZ4CNhH8fXgFkx5COEzz/20wWJmEsSU5oCA==
dependencies:
core-js "^3.38.1"
fflate "^0.4.8"
preact "^10.19.3"
web-vitals "^4.2.0"
preact@^10.19.3:
version "10.25.4"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.25.4.tgz#c1d00bee9d7b9dcd06a2311d9951973b506ae8ac"
integrity sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -11080,7 +11161,7 @@ prosemirror-trailing-node@^3.0.0:
"@remirror/core-constants" "3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3:
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3, prosemirror-transform@^1.9.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz#8ebac4e305b586cd96595aa028118c9191bbf052"
integrity sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==
@@ -13561,6 +13642,11 @@ web-namespaces@^2.0.0:
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
web-vitals@^4.2.0:
version "4.2.4"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7"
integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@@ -148,6 +148,13 @@ ingressAdmin:
enabled: true
host: impress.127.0.0.1.nip.io
posthog:
ingress:
enabled: false
ingressAssets:
enabled: false
ingressMedia:
enabled: true
host: impress.127.0.0.1.nip.io
+1 -1
View File
@@ -93,4 +93,4 @@ releases:
environments:
dev:
values:
- version: 1.10.0
- version: 2.0.1
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 0.0.2
version: 2.0.1-beta.8
appVersion: latest
+36
View File
@@ -104,6 +104,11 @@
| `backend.migrate.restartPolicy` | backend migrate job restart policy | `Never` |
| `backend.createsuperuser.command` | backend migrate command | `["/bin/sh","-c","python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD\n"]` |
| `backend.createsuperuser.restartPolicy` | backend migrate job restart policy | `Never` |
| `backend.job` | job dedicated to run a random management command, for example after a deployment | |
| `backend.job.name` | The name to use to describe this job | `""` |
| `backend.job.command` | The management command to execute | `[]` |
| `backend.job.restartPolicy` | The restart policy for the job. | `Never` |
| `backend.job.annotations` | Annotations to add to the job [default: argocd.argoproj.io/hook: PostSync] | |
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `10` |
@@ -176,6 +181,37 @@
| `frontend.extraVolumeMounts` | Additional volumes to mount on the frontend. | `[]` |
| `frontend.extraVolumes` | Additional volumes to mount on the frontend. | `[]` |
### posthog
| Name | Description | Value |
| -------------------------------------- | ----------------------------------------------------------- | ------------------------- |
| `posthog.ingress.enabled` | Enable or disable the ingress resource creation | `false` |
| `posthog.ingress.className` | Kubernetes ingress class name to use (e.g., nginx, traefik) | `nil` |
| `posthog.ingress.host` | Primary hostname for the ingress resource | `impress.example.com` |
| `posthog.ingress.path` | URL path prefix for the ingress routes (e.g., /) | `/` |
| `posthog.ingress.hosts` | Additional hostnames array to be included in the ingress | `[]` |
| `posthog.ingress.tls.enabled` | Enable or disable TLS/HTTPS for the ingress | `true` |
| `posthog.ingress.tls.additional` | Additional TLS configurations for extra hosts/certificates | `[]` |
| `posthog.ingress.customBackends` | Custom backend service configurations for the ingress | `[]` |
| `posthog.ingress.annotations` | Additional Kubernetes annotations to apply to the ingress | `{}` |
| `posthog.ingressAssets.enabled` | Enable or disable the ingress resource creation | `false` |
| `posthog.ingressAssets.className` | Kubernetes ingress class name to use (e.g., nginx, traefik) | `nil` |
| `posthog.ingressAssets.host` | Primary hostname for the ingress resource | `impress.example.com` |
| `posthog.ingressAssets.paths` | URL paths prefix for the ingress routes (e.g., /static) | `["/static","/array"]` |
| `posthog.ingressAssets.hosts` | Additional hostnames array to be included in the ingress | `[]` |
| `posthog.ingressAssets.tls.enabled` | Enable or disable TLS/HTTPS for the ingress | `true` |
| `posthog.ingressAssets.tls.additional` | Additional TLS configurations for extra hosts/certificates | `[]` |
| `posthog.ingressAssets.customBackends` | Custom backend service configurations for the ingress | `[]` |
| `posthog.ingressAssets.annotations` | Additional Kubernetes annotations to apply to the ingress | `{}` |
| `posthog.service.type` | Service type (e.g. ExternalName, ClusterIP, LoadBalancer) | `ExternalName` |
| `posthog.service.externalName` | External service hostname when type is ExternalName | `eu.i.posthog.com` |
| `posthog.service.port` | Port number for the service | `443` |
| `posthog.service.annotations` | Additional annotations to apply to the service | `{}` |
| `posthog.assetsService.type` | Service type (e.g. ExternalName, ClusterIP, LoadBalancer) | `ExternalName` |
| `posthog.assetsService.externalName` | External service hostname when type is ExternalName | `eu-assets.i.posthog.com` |
| `posthog.assetsService.port` | Port number for the service | `443` |
| `posthog.assetsService.annotations` | Additional annotations to apply to the service | `{}` |
### yProvider
| Name | Description | Value |
+9
View File
@@ -148,6 +148,15 @@ Requires top level scope
{{ include "impress.fullname" . }}-frontend
{{- end }}
{{/*
Full name for the Posthog
Requires top level scope
*/}}
{{- define "impress.posthog.fullname" -}}
{{ include "impress.fullname" . }}-posthog
{{- end }}
{{/*
Full name for the yProvider
+124
View File
@@ -0,0 +1,124 @@
{{- if .Values.backend.job.command -}}
{{- $envVars := include "impress.common.env" (list . .Values.backend) -}}
{{- $fullName := include "impress.backend.fullname" . -}}
{{- $component := "backend" -}}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ $fullName }}-{{ .Values.backend.job.name | default "random" | replace "_" "-" }}
namespace: {{ .Release.Namespace | quote }}
annotations:
argocd.argoproj.io/sync-options: Replace=true,Force=true
{{- with .Values.backend.job.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
spec:
template:
metadata:
annotations:
{{- with .Values.backend.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 8 }}
spec:
{{- if $.Values.image.credentials }}
imagePullSecrets:
- name: {{ include "impress.secret.dockerconfigjson.name" (dict "fullname" (include "impress.fullname" .) "imageCredentials" $.Values.image.credentials) }}
{{- end}}
shareProcessNamespace: {{ .Values.backend.shareProcessNamespace }}
containers:
{{- with .Values.backend.sidecars }}
{{- toYaml . | nindent 8 }}
{{- end }}
- name: {{ .Chart.Name }}
image: "{{ (.Values.backend.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.backend.image | default dict).tag | default .Values.image.tag }}"
imagePullPolicy: {{ (.Values.backend.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
{{- with .Values.backend.job.command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.backend.args }}
args:
{{- toYaml . | nindent 12 }}
{{- end }}
env:
{{- if $envVars}}
{{- $envVars | indent 12 }}
{{- end }}
{{- with .Values.backend.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.backend.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
mountPath: {{ $value.path }}
subPath: content
{{- end }}
{{- range $name, $volume := .Values.backend.persistence }}
- name: "{{ $name }}"
mountPath: "{{ $volume.mountPath }}"
{{- end }}
{{- range .Values.backend.extraVolumeMounts }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
subPath: {{ .subPath | default "" }}
readOnly: {{ .readOnly }}
{{- end }}
{{- with .Values.backend.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.backend.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.backend.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: {{ .Values.backend.job.restartPolicy }}
volumes:
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
configMap:
name: "{{ include "impress.fullname" $ }}-files-{{ $index }}"
{{- end }}
{{- range $name, $volume := .Values.backend.persistence }}
- name: "{{ $name }}"
{{- if eq $volume.type "emptyDir" }}
emptyDir: {}
{{- else }}
persistentVolumeClaim:
claimName: "{{ $fullName }}-{{ $name }}"
{{- end }}
{{- end }}
{{- range .Values.backend.extraVolumes }}
- name: {{ .name }}
{{- if .existingClaim }}
persistentVolumeClaim:
claimName: {{ .existingClaim }}
{{- else if .hostPath }}
hostPath:
{{ toYaml .hostPath | nindent 12 }}
{{- else if .csi }}
csi:
{{- toYaml .csi | nindent 12 }}
{{- else if .configMap }}
configMap:
{{- toYaml .configMap | nindent 12 }}
{{- else if .emptyDir }}
emptyDir:
{{- toYaml .emptyDir | nindent 12 }}
{{- else }}
emptyDir: {}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,86 @@
{{- if .Values.posthog.ingress.enabled -}}
{{- $fullName := include "impress.fullname" . -}}
{{- if and .Values.posthog.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.posthog.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.posthog.ingress.annotations "kubernetes.io/ingress.class" .Values.posthog.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}-posthog
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "impress.labels" . | nindent 4 }}
{{- with .Values.posthog.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.posthog.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.posthog.ingress.className }}
{{- end }}
{{- if .Values.posthog.ingress.tls.enabled }}
tls:
{{- if .Values.posthog.ingress.host }}
- secretName: {{ $fullName }}-posthog-tls
hosts:
- {{ .Values.posthog.ingress.host | quote }}
{{- end }}
{{- range .Values.posthog.ingress.tls.additional }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- if .Values.posthog.ingress.host }}
- host: {{ .Values.posthog.ingress.host | quote }}
http:
paths:
- path: {{ .Values.posthog.ingress.path }}
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ include "impress.posthog.fullname" . }}-proxy
port:
number: {{ .Values.posthog.service.port }}
{{- else }}
serviceName: {{ include "impress.posthog.fullname" . }}-proxy
servicePort: {{ .Values.posthog.service.port }}
{{- end }}
{{- end }}
{{- range .Values.posthog.ingress.hosts }}
- host: {{ . | quote }}
http:
paths:
- path: {{ $.Values.posthog.ingress.path | quote }}
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ include "impress.posthog.fullname" . }}-proxy
port:
number: {{ $.Values.posthog.service.port }}
{{- else }}
serviceName: {{ include "impress.posthog.fullname" . }}-proxy
servicePort: {{ $.Values.posthog.service.port }}
{{- end }}
{{- with $.Values.posthog.service.customBackends }}
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,66 @@
{{- if .Values.posthog.ingressAssets.enabled -}}
{{- $fullName := include "impress.fullname" . -}}
{{- if and .Values.posthog.ingressAssets.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.posthog.ingressAssets.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.posthog.ingressAssets.annotations "kubernetes.io/ingress.class" .Values.posthog.ingressAssets.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}-posthog-assets
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "impress.labels" . | nindent 4 }}
{{- with .Values.posthog.ingressAssets.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.posthog.ingressAssets.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.posthog.ingressAssets.className }}
{{- end }}
{{- if .Values.posthog.ingressAssets.tls.enabled }}
tls:
{{- if .Values.posthog.ingressAssets.host }}
- secretName: {{ $fullName }}-posthog-tls
hosts:
- {{ .Values.posthog.ingressAssets.host | quote }}
{{- end }}
{{- range .Values.posthog.ingressAssets.tls.additional }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- if .Values.posthog.ingressAssets.host }}
- host: {{ .Values.posthog.ingressAssets.host | quote }}
http:
paths:
{{- range .Values.posthog.ingressAssets.paths }}
- path: {{ . | quote }}
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ include "impress.posthog.fullname" $ }}-assets-proxy
port:
number: {{ $.Values.posthog.assetsService.port }}
{{- else }}
serviceName: {{ include "impress.posthog.fullname" $ }}-assets-proxy
servicePort: {{ $.Values.posthog.assetsService.port }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,24 @@
{{- if .Values.posthog.ingressAssets.enabled -}}
{{- $envVars := include "impress.common.env" (list . .Values.posthog) -}}
{{- $fullName := include "impress.posthog.fullname" . -}}
{{- $component := "posthog" -}}
apiVersion: v1
kind: Service
metadata:
name: {{ $fullName }}-assets-proxy
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
annotations:
{{- toYaml $.Values.posthog.assetsService.annotations | nindent 4 }}
spec:
type: {{ .Values.posthog.assetsService.type }}
externalName: {{ .Values.posthog.assetsService.externalName }}
ports:
- port: {{ .Values.posthog.assetsService.port }}
targetPort: {{ .Values.posthog.assetsService.targetPort }}
protocol: TCP
name: https
selector:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 4 }}
{{- end }}
@@ -0,0 +1,24 @@
{{- if .Values.posthog.ingress.enabled -}}
{{- $envVars := include "impress.common.env" (list . .Values.posthog) -}}
{{- $fullName := include "impress.posthog.fullname" . -}}
{{- $component := "posthog" -}}
apiVersion: v1
kind: Service
metadata:
name: {{ $fullName }}-proxy
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "impress.common.labels" (list . $component) | nindent 4 }}
annotations:
{{- toYaml $.Values.posthog.service.annotations | nindent 4 }}
spec:
type: {{ .Values.posthog.service.type }}
externalName: {{ .Values.posthog.service.externalName }}
ports:
- port: {{ .Values.posthog.service.port }}
targetPort: {{ .Values.posthog.service.targetPort }}
protocol: TCP
name: https
selector:
{{- include "impress.common.selectorLabels" (list . $component) | nindent 4 }}
{{- end }}
+84
View File
@@ -251,6 +251,19 @@ backend:
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
restartPolicy: Never
## @extra backend.job job dedicated to run a random management command, for example after a deployment
## @param backend.job.name The name to use to describe this job
## @param backend.job.command The management command to execute
## @param backend.job.restartPolicy The restart policy for the job.
## @extra backend.job.annotations Annotations to add to the job [default: argocd.argoproj.io/hook: PostSync]
## @skip backend.job.annotations.argocd.argoproj.io/hook
job:
name: ""
command: []
restartPolicy: Never
annotations:
argocd.argoproj.io/hook: PostSync
## @param backend.probes.liveness.path [nullable] Configure path for backend HTTP liveness probe
## @param backend.probes.liveness.targetPort [nullable] Configure port for backend HTTP liveness probe
## @param backend.probes.liveness.initialDelaySeconds [nullable] Configure initial delay for backend liveness probe
@@ -390,6 +403,77 @@ frontend:
## @param frontend.extraVolumes Additional volumes to mount on the frontend.
extraVolumes: []
## @section posthog
posthog:
## @param posthog.ingress.enabled Enable or disable the ingress resource creation
## @param posthog.ingress.className Kubernetes ingress class name to use (e.g., nginx, traefik)
## @param posthog.ingress.host Primary hostname for the ingress resource
## @param posthog.ingress.path URL path prefix for the ingress routes (e.g., /)
## @param posthog.ingress.hosts Additional hostnames array to be included in the ingress
## @param posthog.ingress.tls.enabled Enable or disable TLS/HTTPS for the ingress
## @param posthog.ingress.tls.additional Additional TLS configurations for extra hosts/certificates
## @param posthog.ingress.customBackends Custom backend service configurations for the ingress
## @param posthog.ingress.annotations Additional Kubernetes annotations to apply to the ingress
ingress:
enabled: false
className: null
host: impress.example.com
path: /
hosts: [ ]
tls:
enabled: true
additional: [ ]
customBackends: [ ]
annotations: {}
## @param posthog.ingressAssets.enabled Enable or disable the ingress resource creation
## @param posthog.ingressAssets.className Kubernetes ingress class name to use (e.g., nginx, traefik)
## @param posthog.ingressAssets.host Primary hostname for the ingress resource
## @param posthog.ingressAssets.paths URL paths prefix for the ingress routes (e.g., /static)
## @param posthog.ingressAssets.hosts Additional hostnames array to be included in the ingress
## @param posthog.ingressAssets.tls.enabled Enable or disable TLS/HTTPS for the ingress
## @param posthog.ingressAssets.tls.additional Additional TLS configurations for extra hosts/certificates
## @param posthog.ingressAssets.customBackends Custom backend service configurations for the ingress
## @param posthog.ingressAssets.annotations Additional Kubernetes annotations to apply to the ingress
ingressAssets:
enabled: false
className: null
host: impress.example.com
paths:
- /static
- /array
hosts: [ ]
tls:
enabled: true
additional: [ ]
customBackends: [ ]
annotations: {}
## @param posthog.service.type Service type (e.g. ExternalName, ClusterIP, LoadBalancer)
## @param posthog.service.externalName External service hostname when type is ExternalName
## @param posthog.service.port Port number for the service
## @param posthog.service.annotations Additional annotations to apply to the service
service:
type: ExternalName
externalName: eu.i.posthog.com
port: 443
annotations: {}
## @param posthog.assetsService.type Service type (e.g. ExternalName, ClusterIP, LoadBalancer)
## @param posthog.assetsService.externalName External service hostname when type is ExternalName
## @param posthog.assetsService.port Port number for the service
## @param posthog.assetsService.annotations Additional annotations to apply to the service
assetsService:
type: ExternalName
externalName: eu-assets.i.posthog.com
port: 443
annotations: {}
## @section yProvider
yProvider:

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