Compare commits

...

456 Commits

Author SHA1 Message Date
Anthony LC
3cb7aeb7ec 💩(backend) add document content endpoint
Get the content of a document in markdown format.
Ex: http://localhost:8071/api/v1.0/documents/<ID>/content/
2025-06-03 12:12:19 +02:00
Anthony LC
23860065e1 env.example 2025-06-03 11:19:18 +02:00
Anthony LC
f459c56121 📝(mcp) add doc to use mcp with vscode 2025-05-30 17:48:16 +02:00
Quentin BEY
4a81e1526e 👷(hackdays) publish the MCP docker image
Publish the MCP Docker image on our registry.
2025-05-28 11:29:38 +02:00
Quentin BEY
abcd61cf2f 👷(hackdays) publish the docker image
Publish the Docker images to deploy on a dedicated instance for the
Hackdays.
2025-05-28 11:29:38 +02:00
Quentin BEY
c1a591fb4f 🧱(mcp) add server deployment
Provide the helm chart declaration to deploy the MCP server.
2025-05-28 11:29:38 +02:00
Quentin BEY
83d8478b5d 💩(mcp) add a local MCP server configuration
This provides a way to start a local MCP server:
 - provided a user token, the MCP can create document
 - can be run locally and work with cursor or mcphost
2025-05-28 11:29:38 +02:00
Quentin BEY
6bd136c76e 💩(user-tokens) add back & front for Token auth
This provides:
 - a frontend to allow user to create/delete User Token
 - the authentication process to allow any API to be called when
   authenticating with a User Token.
2025-05-26 14:51:08 +02:00
Quentin BEY
e929fcc682 💩(resource-server) open all APIs to RS
This provides a base configuration to allow to access all
API via OIDC resource server authentication.
2025-05-26 11:39:43 +02:00
Quentin BEY
fa819bc1ff 🐛(auth) allow several auth backend on m2m API
The previous `ServerToServerAuthentication` was raising authentication
failed error if anything is wrong (the header, the token) which prevents
any possibility to have several authentication backends.
2025-05-26 11:39:43 +02:00
Quentin BEY
43e529da2a 🔒️(oidc) disable OIDC authentication on API
Our authentication flow uses the Django authentication which creates a
session for the User. Then the session is used to make API calls,
therefore we don't need to accept OIDC tokens directly on the API.

Accepting the OIDC token on the API can allow to bypass the "resource
server mode" which allows to restrict provided information according to
the Service Provider which makes the request.
2025-05-26 11:39:43 +02:00
renovate[bot]
cde64ed80a ⬆️(dependencies) update js dependencies 2025-05-26 06:39:40 +00:00
renovate[bot]
cfd88d0469 ⬆️(dependencies) update python dependencies 2025-05-26 01:55:36 +00:00
virgile-dev
5e45fec296 📝(doc) fix path to env doc on readme (#1007)
The path lead to a 404

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-25 17:01:29 +00:00
Anthony LC
393e7a06e2 🔖(minor) release 3.3.0
Added:
- (backend) add endpoint checking media status
- (backend) allow setting session cookie age via env var
- (backend) allow theme customnization using a
configuration file
- (frontend) Add a custom callout block to the editor
- 🚩(frontend) version MIT only
- (backend) integrate maleware_detection from django-lasuite
- 🏗️(frontend) Footer configurable
- 🩺(CI) add lint spell mistakes
- (frontend) create generic theme
- 🛂(frontend) block edition to not connected users
- 🚸(frontend) Let loader during upload analyze
- 🚩(frontend) feature flag on blocking edition

Changed:
- 📝(frontend) Update documentation
- (frontend) Improve tests coverage
- ⬆️(docker) upgrade backend image to python 3.13
- ⬆️(docker) upgrade node images to alpine 3.21

Fixed:
- 🐛(y-provider) increase JSON size limits for
transcription conversion

Removed:
- 🔥(back) remove footer endpoint
2025-05-23 11:41:24 +02:00
AntoLC
f1af87baf8 🌐(i18n) update translated strings
Update translated files with new translations
2025-05-23 11:41:24 +02:00
Anthony LC
f851ef2d85 ⬆️(dependencies) bump blocknote to 0.30.1
A bunch of fixes are in this pacth release.
Better to update now before we release to 3.3.0.
2025-05-23 11:08:07 +02:00
Anthony LC
252ab6a586 ✏️(frontend) change antivirus fail sentence
The message was not accurate when the analizer failed.
We improved the message to be more accurate.
2025-05-23 11:08:07 +02:00
Anthony LC
cf2a02c8de 🚩(frontend) feature flag on blocking edition
If users were not connected to the collaboration
server, they were not be able to edit documents.

We decided to add a feature flag on this feature
as it can be quite restrictive.
We can now enable or disable this feature at runtime
thanks to the env variable
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY".
2025-05-23 11:08:07 +02:00
Anthony LC
d87a2ed4eb 🔥(helm) remove useless footer config
Remove:
- FRONTEND_FOOTER_FEATURE_ENABLED
- FRONTEND_FOOTER_URL
2025-05-22 15:27:39 +02:00
Anthony LC
c9d053d1c0 💄(frontend) add generic favicon
The favicons were still with the dsfr color.
We added the generic favicon in the assets folder.
The favicon can be a url loaded from the theme,
so when Drive will be running, we will be able
to store the dsfr favicons there, and remove them
from the repo.
2025-05-22 15:27:38 +02:00
Anthony LC
b5f0f06ea3 💄(frontend) desaturate images system for generic theme
We want to desaturate the images system in the
generic theme to make them less colorful and more
in line with the overall theme.
We added a special class to the images
that need to be desaturated. Other property
then desaturated can be apply depending on the theme.
2025-05-22 15:27:38 +02:00
Anthony LC
36b0ff9f63 (frontend) create generic theme
By default Docs will not be on the dsfr theme but
on the generic theme. La Gaufre is part of the dsfr
theme and is removed from the generic theme.
Same for the "beta" keyword and the "proconnect"
buttons.
2025-05-22 15:27:38 +02:00
Anthony LC
7a383957a7 🔥(frontend) remove legal pages
Legal pages are not needed anymore in the application.
In the dsfr instances, the legal pages will be
displayed on a Docs pages.
We let the users of Docs managing the legal pages
on their own instances.
2025-05-22 14:07:41 +02:00
Anthony LC
b5630359ee 🏗️(frontend) Footer configurable
To have different footer per instance the
content of the footer is now configurable
from the theme customization file.

See THEME_CUSTOMIZATION_FILE_PATH env var.
2025-05-22 14:07:41 +02:00
Anthony LC
310154815b ♻️(e2e) improve config testcases
Improve config testcases:
- let THEME_CUSTOMIZATION_FILE_PATH to be set to
check the default value
- add helper function overrideConfig
2025-05-22 14:07:41 +02:00
Anthony LC
2733785016 🚨(linter) add ignore pattern on no-unused-vars rule
The rule @typescript-eslint/no-unused-vars didn't
have a ignore pattern. A ignore pattern can be
usefull in some cases.
2025-05-22 14:07:41 +02:00
Manuel Raynaud
99ba414d88 🔧(back) add docs.security to logging settings
In the malware_detection callback we are using a different logger named
docs.security. We want to configure a logger in the logging settings
handling it.
2025-05-22 13:53:27 +02:00
Manuel Raynaud
41631b5b70 ⬆️(backend) upgrade django-lasuite to version 0.0.9
We need version 0.0.9 to reduce the time to have a JCOP analysis result.
2025-05-22 13:53:27 +02:00
Anthony LC
6ca654bf1a 🚸(frontend) let loader until resource ready
The backend can analyze the upload file, this take
time, so we need to show a loader
until the backend finish the analysis.
2025-05-22 13:53:27 +02:00
Manuel Raynaud
074585337b ♻️(back) return the media-check url on the attachment_upload response
We want to have the media-check url returned on the attachment-upload
response instead of the media url directly. The front will know the
endpoint to use to check the media status.
2025-05-22 13:39:44 +02:00
Manuel Raynaud
f1b398e1ae (back) add endpoint checking media status
With the usage of a malware detection system, we need a way to know the
file status. The front will use it to display a loader while the analyse
is not ended.
2025-05-22 13:39:44 +02:00
Jacques ROUSSEL
d1f73f18cd 🔒️(front) improve docker image security
Cyberwatch reported security issues with the frontend Docker image.
2025-05-22 11:16:57 +02:00
lebaudantoine
3f2d84bf62 🐛(y-provider) increase JSON size limits for transcription conversion
Problem:
- Default Express JSON parser limit (100kb) is insufficient for larger
 transcription files
- 2-hour audio transcriptions slightly exceed the 100kb limit, causing request
 failures

Solution:
- Implemented custom middleware to apply different JSON parser configurations
 based on route
- Applied 500kb limit specifically for transcription conversion endpoints
- Maintained default limits for all other routes to preserve security

Technical notes:
- Could not find a built-in Express solution to specify parser config per route
- Custom middleware conditionally applies the appropriate parser configuration
2025-05-21 15:31:49 +02:00
lebaudantoine
7b9c362d38 🐛(tilt) update certificate path for Python 3.13 upgrade
Fix certificate directory reference that still pointed to Python 3.12 folder
after upgrading to Python 3.13. Resolves certificate verification errors in
tilt stack caused by incorrect certificate location.
2025-05-21 12:53:41 +02:00
virgile-dev
bf999979d2 📝(doc) update xl packages warning (#985)
So that people know how to use the PUBLISH_AS_MIT variable

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-20 21:10:54 +02:00
renovate[bot]
09d3ff3754 ⬆️(dependencies) update python dependencies 2025-05-19 12:21:04 +00:00
Samuel Paccoud - DINUM
6e5d005dee (backend) allow setting session cookie age via env var
We want to be able to increase the duration of the cookie session
by setting an environment variable.
2025-05-19 13:57:30 +02:00
Anthony LC
6377c8fcca ✈️(frontend) allow editing when offline
When the user is offline, we allow editing the
document in the editor.
Their is not a reliable way to know if the user is
offline or online except by doing a network request
and checking if an error is thrown or not.
To do so, we created the OfflinePlugin inherited
from the WorkboxPlugin.
It will inform us if the user is offline or online.
We then dispatch the information to our application
thanks to the useOffline hook.
2025-05-19 12:36:32 +02:00
Anthony LC
3c8cacc048 🛂(frontend) block edition to not connected users
If an editor is working on a shared document but
is not connected to the collaborative server
we are now blocking the edition.
It is to avoid none connected users to
overwrite the document with connected
users.
2025-05-19 12:36:31 +02:00
virgile-dev
598fb4fa27 📝(doc) update issue templates (#976)
Mention of Impress instead of Docs is confusing. Also added some
automatic labelling.

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-19 09:14:00 +00:00
Anthony LC
51618ad081 📌(dependencies) add hocuspocus to renovate ignored list
hocuspocus > 3.0.0 brings breaking changes.
Let's add it in the renovate.json ignored list
until we decide to upgrade it.
2025-05-19 09:19:37 +02:00
renovate[bot]
8109d5ba08 ⬆️(dependencies) update js dependencies 2025-05-19 09:17:33 +02:00
virgile-dev
e4d0179bbe 📝(doc) readme update (#974)
Added badges so people can assess the repo activity easily. 
Also update the paragraph about AGPL XL packages since this was merged :
https://github.com/suitenumerique/docs/pull/911

---------

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-16 17:48:57 +00:00
Manuel Raynaud
9d3dfb6de7 ⬆️(docker) upgrade node images to alpine 3.21
We need to upgrade our images to alpine 3.21 in order to fix a CVE
related to libxml2. We also upgrade node to version 24
2025-05-16 15:55:33 +02:00
Manuel Raynaud
0da042f887 ⬆️(docker) upgrade backend image to python 3.13
Python 3.13 is now stable, our libraries are compatible with it. We also
upgrade the alpine version used in order to fix CVE related to libxml2
2025-05-16 15:55:32 +02:00
Anthony LC
6cd0cd0689 ⬆️(dependencies) gouvfr-lasuite/ui-kit to 0.6.0
Upgrade @gouvfr-lasuite/ui-kit from 0.5.0 to 0.6.0.
Some properties have been removed in 0.5.0, which
causes the design of the app to be broken.
Version 0.6.0 has as well some breaking changes,
about the "logo" properties that are not available anymore.
We fix them in this commit.
2025-05-16 12:35:16 +02:00
Anthony LC
10b088599c 🐛(frontend) fix svg export
Last upgrade of Blocknote to 0.30.0 broke the SVG
export. The previewWidth can be undefined, which causes the
export to fail. This commit adds a fallback
width in case previewWidth is undefined.
2025-05-16 11:12:30 +02:00
Anthony LC
62d1bc6473 🐛(frontend) redirect to /home
The page '/login' was replaced with '/home',
but some users may still have the old URL in their
bookmarks, it can create a loop during the
authentication process.
We redirect the user to '/home' if they try to access
'/login' page, it will prevent edge cases.
2025-05-16 11:12:30 +02:00
Anthony LC
fc1d33268c ⬆️(dependencies) update js dependencies 2025-05-16 11:12:30 +02:00
virgile-dev
95833fa5ec 📝(documentation) add banner to readme (#970)
Implement suggestions made by @xibe in
[#848](https://github.com/suitenumerique/docs/pull/848) and
[#849](https://github.com/suitenumerique/docs/pull/849)

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-05-16 11:10:05 +02:00
Anthony LC
dd6e0b5072 📝(project) add missing env var to env.md
Update the documentation to include the
missing environment variables.
The missing environment variables are involved
in the build process of the frontend image.
2025-05-13 22:26:08 +02:00
Anthony LC
95d3a8cd18 ✏️(project) automatic typo correction
Fix typos in the project.
2025-05-13 16:00:43 +02:00
Anthony LC
4f126ab824 🩺(CI) add lint spell mistakes
We get lot of pull requests about typo.
We add codespell linter in the CI, it will inform
us if we introduce spell mistakes.
2025-05-13 16:00:43 +02:00
Manuel Raynaud
fb90c13dad ♻️(helm) change default customization CM mount path
The mount path used in the backend deployment to mount the customization
file ConfigMap is not the same from the default settings. To avoid extra
configuration we change it to refrlect the default value of
settings.THEME_CUSTOMIZATION_FILE_PATH
2025-05-13 15:19:55 +02:00
Manuel Raynaud
4118d79525 🔧(helm) add celery deployment
We need to configure a deployment dedicated to celery. It is a copy of
the backend one with modification made where it is specific to celery
2025-05-13 15:19:54 +02:00
renovate[bot]
5848f43cb4 ⬆️(dependencies) update python dependencies (#956) 2025-05-12 14:29:04 +00:00
Manuel Raynaud
4b0fd223c8 🐛(back) override AI feature flag in config test
The env.d/development/common file sets
AI_FEATURE_ENABLED=true.
When pytest starts it imports these variables, so
the /api/v1.0/config endpoint returns
AI_FEATURE_ENABLED=True and the test_api_config
assertion fails.

Explicitly overriding AI_FEATURE_ENABLED=False in
test_api_config restores the expected behaviour
and makes the whole test-suite green.

Signed-off-by: ReinforcedKnowledge <reinforced.knowledge@gmail.com>
2025-05-12 15:56:30 +02:00
Manuel Raynaud
31d0733851 🔧(back) configure cache key prefix
We want to change the cache key prefix using an environment variable.
This settings can be changed at every deployment in order to reset to
use a fresh new cache.
2025-05-12 15:56:29 +02:00
Manuel Raynaud
16e20e984c (helm) allow to load custom theme file in a configMap
In order to load a custom theme file with our helm chart, we allow to
load the content of a file into a config map and then use this configmap
as a volume in the backend deployment
2025-05-12 15:56:29 +02:00
Manuel Raynaud
76c28760dc 🔥(back) remove footer endpoint
With the configuration file, the footer endpoint can be removed and will
not be used anymore by the front application.
2025-05-12 15:56:29 +02:00
Manuel Raynaud
d856abb5d8 (back) allow theme customnization using a configuration file
We want to customize the theme by using a configuration file. This
configuration file path can be defined using the settings
THEME_CUSTOMIZATION_FILE_PATH. If this file does not exists or is an
invalid json, an empty json object will be added in the config endpoint.
2025-05-12 15:56:26 +02:00
Manuel Raynaud
25abd964de (backend) manage uploaded file status and call to malware detection
In the attachment_upload method, the status in the file metadata to
processing and the malware_detection backend is called. We check in the
media_auth if the status is ready in order to accept the request.
2025-05-12 15:14:09 +02:00
Manuel Raynaud
a070e1dd87 (backend) configure lasuite.malware_detection module
We want to use the malware_detection module from lasuite library. We add
a new setting MALWARE_DETECTION to configure the backend we want to use.
The callback is also added. It removes the file if it is not safe or
change it's status in the metadata to set it as ready.
2025-05-12 15:13:33 +02:00
Manuel Raynaud
37d9ae8cca (backend) force loading celery shared task in libraries
Library we are using can have celery shared task. We have to make some
modification to load them earlier when the celery app is configure and
when the impress app is loaded.
2025-05-12 15:13:32 +02:00
Zorin95670
29ea6b8ef7 (frontend) Improve test coverage
Improve the test coverage of the "api" modules.

Signed-off-by: Zorin95670 <moittie.vincent@gmail.com>
2025-05-12 14:07:08 +02:00
Zorin95670
a692fa6f39 📝(frontend) Update documentation
Improve and add jsdoc.

Signed-off-by: Zorin95670 <moittie.vincent@gmail.com>
2025-05-12 14:07:08 +02:00
Zorin95670
4d541c5d52 🎨(frontend) Minor refactoring
- improve condition statements
- add "no-var" rule in eslint
- remove some unnecessary variables

Signed-off-by: Zorin95670 <moittie.vincent@gmail.com>
2025-05-12 14:07:08 +02:00
Anthony LC
e5f029ad1d 🚩(frontend) version MIT only
We have some packages that are not MIT compatible,
so if the env var MIT_ONLY is set to true,
we don't build the application with features
that are not MIT compatible.
For the moment, it concerns only the export packages.
2025-05-12 12:00:59 +02:00
ZouicheOmar
bd79f84e07 (frontend) adapt export to callout block
Adapt modal export to include PDF and Docx export
for the callout block.
2025-05-12 09:30:17 +02:00
ZouicheOmar
a070f56339 (frontend) add custom callout block to editor
Add a custom block to the editor, the callout block.
2025-05-12 09:30:17 +02:00
ZouicheOmar
02478acb3f (frontend) add emoji picker component
Add a custom emoji picker component to use in the editor
2025-05-12 09:30:17 +02:00
ZouicheOmar
23aa497db0 (frontend) add emoji-mart packages
We need functionalities and data to implement a custom emoji picker
component, as blocknote's emojipicker component triggers and uses cases
are limited.
add to package.json the following packages:
- "emoji-mart": provides functions and components for
displaying, searching and selecting emojis.
- @emoji-mart-data: offers pre-configured sets of emojis.
- @emoji-mart/react: React Picker component
2025-05-12 09:29:04 +02:00
virgile-dev
d48436bffb 📝(doc) complete contributing policy (#895)
We made mandatory signing commits.
Provided warnings for common gitmoji errors

Signed-off-by: virgile-deville <virgile.deville@beta.gouv.fr>
2025-05-09 20:40:44 +00:00
renovate[bot]
41e4c45934 ⬆️(dependencies) update django to v5.1.9 [SECURITY] (#953) 2025-05-09 16:26:57 +02:00
Anthony LC
6be87ed477 🔖(patch) release 3.2.1
Fixed:
- 🐛(frontend) fix list copy paste
2025-05-07 10:27:39 +02:00
Anthony LC
c96182b3e3 🐛(frontend) fix list copy paste
When we copy paste a list, the pasted
list is not formatted correctly.
By pinning prosemirror-model to 1.25.0,
we avoid this issue.
We added "prosemirror-model" to the
ignored dependencies of Renovate to
avoid to have a bump until the patch
on the Blocknote.js side.
2025-05-07 10:25:48 +02:00
Anthony LC
e79d1d618a ⬆️(dependencies) update js dependencies 2025-05-06 11:51:24 +02:00
renovate[bot]
2691cdd4a2 ⬆️(dependencies) update python dependencies (#934) 2025-05-06 09:35:31 +00:00
Riël Notermans
05a1390bdc 📝(doc) Update env.md add AI_FEATURE_ENABLED
This is false by default.
Without this env setting on true AI will not be available in the
docs application.
The setting was missing in the env options.
2025-05-06 10:54:18 +02:00
Anthony LC
dfe8ae14fe 🐛(docker-compose) unbind the y-provider service with frontend
We cannot add new js dependency locally when we bind the
frontend with the y-provider service. It results in
"EPERM: operation not permitted" when the `node_modules`
has to be updated.
Better to remove the binding, we can add the binding
locally during development on the y-provider.
2025-05-06 10:35:59 +02:00
Anthony LC
74165f6890 🔖(minor) release 3.2.0
Added:
- 🚸(backend) make document search on title
  accent-insensitive
- 🚩 add homepage feature flag
- (settings) Allow configuring PKCE for the SSO
- 🌐(i18n) activate chinese and spanish languages
- 🔧(backend) allow overwriting the data directory
- (backend) add  `django-lasuite` dependency
  (breaking change)
- (frontend) advanced table features

Changed:
- ️(frontend) reduce unblocking time for config
- ♻️(frontend) bind UI with ability access
- ♻️(frontend) use built-in Quote block

Fixed:
- 🐛(nginx) fix 404 when accessing a doc
- 🔒️(drf) disable browsable HTML API renderer
- 🔒(frontend) enhance file download security
- 🐛(backend) race condition create doc
- 🐛(frontend) fix breaklines in custom blocks
2025-05-06 09:33:42 +02:00
Anthony LC
349cbf8eb3 🌐(i18n) update translated strings
Update translated files with new translations
2025-05-06 09:33:42 +02:00
Anthony LC
12ef1a2450 🚩(backend) default enable FRONTEND_HOMEPAGE_FEATURE_ENABLED
We decided to enable the FRONTEND_HOMEPAGE_FEATURE_ENABLED
feature flag by default.
It will not be a breaking change like that.
2025-05-05 11:54:26 +02:00
Anthony LC
9b2f7966f6 🌐(i18n) update translated strings
Update translated files with new translations
2025-05-05 11:17:58 +02:00
Anthony LC
5ad30b404d 🌐(i18n) add PO of new languages
New languages were added to Crowdin.
We import the new translations from Crowdin
to version them in the repository.
2025-05-02 16:25:50 +02:00
Anthony LC
12524f35b7 🌐(i18n) remove chinese language
We're going to make languages ​​configurable
per instance, but until we manage that, we're going
to remove Chinese from the default language list.

- Remove the chinese language from the default language
list.
- Change Spanish to Español
2025-05-02 16:25:50 +02:00
Anthony LC
f8a40cf8cc (frontend) add advanced table features
We added advanced table features to the
table editor, including:
- split / merge cells
- cell background color
- cell text color
- header

We adapted the export and brought some improvements
compare to the previous version.

The export PDF supports colspan (merge horizontally),
but does not support the rowspan (merge vertically)
for now.
2025-04-30 17:22:21 +02:00
Anthony LC
c32fdb67ac (frontend) add @blocknote/code-block
To reduce the bundles size, the highlight syntax
library is not included in blocknote core anymore.
We need to add a separate dependency in order
to have the code block syntax highlight feature.
2025-04-30 17:22:21 +02:00
Anthony LC
7f2a21cdc9 🔥(frontend) remove Quote custom block
Last Blocknote upgrade included a Quote block,
better to use their built-in one.
2025-04-30 17:22:21 +02:00
Anthony LC
4ad917906c ⬆️(dependencies) update js dependencies 2025-04-30 17:22:21 +02:00
Anthony LC
9ca79688c9 ♻️(frontend) bind ui with ability access
Some actions were not available in the frontend
but allowed in the backend, this commit binds the frontend
ui with the ability access coming from the backend.
2025-04-30 17:02:13 +02:00
Manuel Raynaud
7f0eb9117e 🔒️(drf) disable browsable HTML API renderer (#919)
The `BrowsableAPIRenderer` generates a form to test POST/PUT/... actions
and fill the FK fields with unfiltered data. This issue has been spoted
on visio and fixed suitenumerique/meet#508
2025-04-30 16:23:26 +02:00
Quentin BEY
2557c6bc77 (backend) add django-lasuite dependency
Use the OIDC backend from the new library and add settings to setup OIDC
token storage required for later calls to OIDC Resource Servers.
2025-04-29 13:15:43 +02:00
Manuel Raynaud
df173c3ce6 🔧(helmfile) personalize keycloak configuration
The keycloak configuration used in dev environment is too generic and we
can have a conflict with other project that are using the same ingress
domain. Also the namespace was missing in the keycloak extra ConfigMap
leading to creating it in the default namespace.
2025-04-28 21:41:02 +02:00
Anthony LC
b58c991c81 🐛(nginx) fix 404 when accessing a doc
We improve the nginx way to access to a specific
doc.
We stop to wait for a initial attempt that
give a 404. If we see a UUID in the url we will
redirect to the doc/[id] page. Next will then
manage the 404.
2025-04-28 21:41:02 +02:00
Martin Weinelt
96f6aeea60 🔧(backend) Allow overwriting the data directory (#893)
## Purpose

Deployments that don't rely on Docker should be given the option to use
a different data directory.

## Proposal

Allow customization of the `DATA_DIR` through an environment variable of
the same name.

If the environment variable is not set the behaviour remains the same as
before.

Signed-off-by: Martin Weinelt <hexa@darmstadt.ccc.de>
2025-04-28 15:41:28 +00:00
Nathan Panchout
9465f1a6ec 🔒(frontend) enhance file download security (#889)
## Purpose

Added a safety check for URLs in the FileDownloadButton component. Now,
before opening a URL, it verifies if the URL is safe using the isSafeUrl
function.
This prevents potentially unsafe URLs from being opened in a new tab.
2025-04-28 12:50:14 +00:00
virgile-dev
98f11ff8ac 🌐(i18n) add spanish and chinese (#884)
All the spanish and chinese translations are complete on crowdin. We
activate it in django settings and download all translations from
crowdin

Signed-off-by: virgile-deville <virgile.deville@beta.gouv.fr>
2025-04-28 12:36:34 +00:00
renovate[bot]
b29daa2d77 ⬆️(dependencies) update python dependencies (#847)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [boto3](https://redirect.github.com/boto/boto3) | `==1.37.24` ->
`==1.37.33` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/boto3/1.37.33?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/boto3/1.37.33?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/boto3/1.37.24/1.37.33?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/boto3/1.37.24/1.37.33?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [celery](https://docs.celeryq.dev/)
([source](https://redirect.github.com/celery/celery),
[changelog](https://docs.celeryq.dev/en/stable/changelog.html)) |
`==5.5.0` -> `==5.5.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/celery/5.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/celery/5.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/celery/5.5.0/5.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/celery/5.5.0/5.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [django](https://redirect.github.com/django/django)
([changelog](https://docs.djangoproject.com/en/stable/releases/)) |
`==5.1.8` -> `==5.2` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/django/5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/django/5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/django/5.1.8/5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/django/5.1.8/5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[django-extensions](https://redirect.github.com/django-extensions/django-extensions)
([changelog](https://redirect.github.com/django-extensions/django-extensions/blob/main/CHANGELOG.md))
| `==3.2.3` -> `==4.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/django-extensions/4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/django-extensions/4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/django-extensions/3.2.3/4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/django-extensions/3.2.3/4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[django-storages](https://redirect.github.com/jschneier/django-storages)
([changelog](https://redirect.github.com/jschneier/django-storages/blob/master/CHANGELOG.rst))
| `==1.14.5` -> `==1.14.6` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/django-storages/1.14.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/django-storages/1.14.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/django-storages/1.14.5/1.14.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/django-storages/1.14.5/1.14.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[drf-spectacular-sidecar](https://redirect.github.com/tfranzel/drf-spectacular-sidecar)
| `==2025.3.1` -> `==2025.4.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/drf-spectacular-sidecar/2025.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/drf-spectacular-sidecar/2025.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/drf-spectacular-sidecar/2025.3.1/2025.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/drf-spectacular-sidecar/2025.3.1/2025.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [ipython](https://redirect.github.com/ipython/ipython) | `==9.0.2` ->
`==9.1.0` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ipython/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/ipython/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/ipython/9.0.2/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ipython/9.0.2/9.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [lxml](https://lxml.de/)
([source](https://redirect.github.com/lxml/lxml),
[changelog](https://git.launchpad.net/lxml/plain/CHANGES.txt)) |
`==5.3.1` -> `==5.3.2` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/lxml/5.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/lxml/5.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/lxml/5.3.1/5.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/lxml/5.3.1/5.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [markdown](https://redirect.github.com/Python-Markdown/markdown)
([changelog](https://python-markdown.github.io/changelog/)) | `==3.7` ->
`==3.8` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/markdown/3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/markdown/3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/markdown/3.7/3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/markdown/3.7/3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [openai](https://redirect.github.com/openai/openai-python) |
`==1.70.0` -> `==1.73.0` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/openai/1.73.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/openai/1.73.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/openai/1.70.0/1.73.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/openai/1.70.0/1.73.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [pycrdt](https://redirect.github.com/jupyter-server/pycrdt) |
`==0.12.10` -> `==0.12.12` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/pycrdt/0.12.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pycrdt/0.12.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pycrdt/0.12.10/0.12.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pycrdt/0.12.10/0.12.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [pytest-cov](https://redirect.github.com/pytest-dev/pytest-cov)
([changelog](https://pytest-cov.readthedocs.io/en/latest/changelog.html))
| `==6.0.0` -> `==6.1.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/pytest-cov/6.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pytest-cov/6.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pytest-cov/6.0.0/6.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pytest-cov/6.0.0/6.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [pytest-django](https://redirect.github.com/pytest-dev/pytest-django)
([changelog](https://pytest-django.readthedocs.io/en/latest/changelog.html))
| `==4.10.0` -> `==4.11.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/pytest-django/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pytest-django/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pytest-django/4.10.0/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pytest-django/4.10.0/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [ruff](https://docs.astral.sh/ruff)
([source](https://redirect.github.com/astral-sh/ruff),
[changelog](https://redirect.github.com/astral-sh/ruff/blob/main/CHANGELOG.md))
| `==0.11.2` -> `==0.11.5` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/ruff/0.11.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/ruff/0.11.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/ruff/0.11.2/0.11.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/ruff/0.11.2/0.11.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [sentry-sdk](https://redirect.github.com/getsentry/sentry-python)
([changelog](https://redirect.github.com/getsentry/sentry-python/blob/master/CHANGELOG.md))
| `==2.25.0` -> `==2.25.1` |
[![age](https://developer.mend.io/api/mc/badges/age/pypi/sentry-sdk/2.25.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/sentry-sdk/2.25.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/sentry-sdk/2.25.0/2.25.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/sentry-sdk/2.25.0/2.25.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>boto/boto3 (boto3)</summary>

###
[`v1.37.33`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13733)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.32...1.37.33)

\=======

- api-change:`connect-contact-lens`: \[`botocore`] Making sentiment
optional for ListRealtimeContactAnalysisSegments Response depending on
conversational analytics configuration
- api-change:`datazone`: \[`botocore`] Raise hard limit of authorized
principals per SubscriptionTarget from 10 to 20.
- api-change:`detective`: \[`botocore`] Add support for Detective
DualStack endpoints
- api-change:`dynamodb`: \[`botocore`] Doc only update for API
descriptions.
- api-change:`marketplace-entitlement`: \[`botocore`] Add support for
Marketplace Entitlement Service dual-stack endpoints for CN and GOV
regions
- api-change:`meteringmarketplace`: \[`botocore`] Add support for
Marketplace Metering Service dual-stack endpoints for CN regions
- api-change:`pcs`: \[`botocore`] Changed the minimum length of
clusterIdentifier, computeNodeGroupIdentifier, and queueIdentifier to 3.
- api-change:`verifiedpermissions`: \[`botocore`] Adds deletion
protection support to policy stores. Deletion protection is disabled by
default, can be enabled via the CreatePolicyStore or UpdatePolicyStore
APIs, and is visible in GetPolicyStore.
- bugfix:`download_fileobj`: Fileobj provided in append mode will no
longer allow concurrent writes to preserve data integrity.

###
[`v1.37.32`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13732)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.31...1.37.32)

\=======

- api-change:`application-autoscaling`: \[`botocore`] Application Auto
Scaling now supports horizontal scaling for Elasticache Memcached
self-designed clusters using target tracking scaling policies and
scheduled scaling.
- api-change:`elasticache`: \[`botocore`] AWS ElastiCache SDK now
supports using MemcachedUpgradeConfig parameter with ModifyCacheCluster
API to enable updating Memcached cache node types. Please refer to
updated AWS ElastiCache public documentation for detailed information on
API usage and implementation.
- api-change:`m2`: \[`botocore`] Introduce three new APIs:
CreateDataSetExportTask, GetDataSetExportTask and
ListDataSetExportHistory. Add support for batch restart for Blu Age
applications.
- api-change:`medialive`: \[`botocore`] AWS Elemental MediaLive /
Features : Add support for CMAF Ingest CaptionLanguageMappings,
TimedMetadataId3 settings, and Link InputResolution.
- api-change:`qbusiness`: \[`botocore`] Adds functionality to
enable/disable a new Q Business Hallucination Reduction feature. If
enabled, Q Business will detect and attempt to remove Hallucinations
from certain Chat requests.
- api-change:`quicksight`: \[`botocore`] Add support to analysis and
sheet level highlighting in QuickSight.

###
[`v1.37.31`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13731)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.30...1.37.31)

\=======

- api-change:`controlcatalog`: \[`botocore`] The GetControl API now
surfaces a control's Severity, CreateTime, and Identifier for a
control's Implementation. The ListControls API now surfaces a control's
Behavior, Severity, CreateTime, and Identifier for a control's
Implementation.
- api-change:`dynamodb`: \[`botocore`] Documentation update for
secondary indexes and Create_Table.
- api-change:`glue`: \[`botocore`] The TableOptimizer APIs in AWS Glue
now return the DpuHours field in each TableOptimizerRun, providing
clients visibility to the DPU-hours used for billing in managed Apache
Iceberg table compaction optimization.
- api-change:`groundstation`: \[`botocore`] Support tagging Agents and
adjust input field validations
- api-change:`transfer`: \[`botocore`] This launch includes 2
enhancements to SFTP connectors user-experience: 1) Customers can
self-serve concurrent connections setting for their connectors, and 2)
Customers can discover the public host key of remote servers using their
SFTP connectors.

###
[`v1.37.30`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13730)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.29...1.37.30)

\=======

- api-change:`bedrock-runtime`: \[`botocore`] This release introduces
our latest bedrock runtime API, InvokeModelWithBidirectionalStream. The
API supports both input and output streams and is supported by only
HTTP2.0.
- api-change:`ce`: \[`botocore`] This release supports Pagination traits
on Cost Anomaly Detection APIs.
- api-change:`cost-optimization-hub`: \[`botocore`] This release adds
resource type "MemoryDbReservedInstances" and resource type
"DynamoDbReservedCapacity" to the GetRecommendation,
ListRecommendations, and ListRecommendationSummaries APIs to support new
MemoryDB and DynamoDB RI recommendations.
- api-change:`iotfleetwise`: \[`botocore`] This release adds the option
to update the strategy of state templates already associated to a
vehicle, without the need to remove and re-add them.
- api-change:`securityhub`: \[`botocore`] Documentation updates for AWS
Security Hub.
- api-change:`storagegateway`: \[`botocore`] Added new
ActiveDirectoryStatus value, ListCacheReports paginator, and support for
longer pagination tokens.
- api-change:`taxsettings`: \[`botocore`] Uzbekistan Launch on
TaxSettings Page

###
[`v1.37.29`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13729)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.28...1.37.29)

\=======

- api-change:`bedrock`: \[`botocore`] New options for how to handle
harmful content detected by Amazon Bedrock Guardrails.
- api-change:`bedrock-runtime`: \[`botocore`] New options for how to
handle harmful content detected by Amazon Bedrock Guardrails.
- api-change:`codebuild`: \[`botocore`] AWS CodeBuild now offers an
enhanced debugging experience.
- api-change:`glue`: \[`botocore`] Add input validations for multiple
Glue APIs
- api-change:`medialive`: \[`botocore`] AWS Elemental MediaLive now
supports SDI inputs to MediaLive Anywhere Channels in workflows that use
AWS SDKs.
- api-change:`personalize`: \[`botocore`] Add support for eventsConfig
for CreateSolution, UpdateSolution, DescribeSolution,
DescribeSolutionVersion. Add support for GetSolutionMetrics to return
weighted NDCG metrics when eventsConfig is enabled for the solution.
- api-change:`transfer`: \[`botocore`] This launch enables customers to
manage contents of their remote directories, by deleting old files or
moving files to archive folders in remote servers once they have been
retrieved. Customers will be able to automate the process using
event-driven architecture.

###
[`v1.37.28`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13728)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.27...1.37.28)

\=======

- api-change:`ds-data`: \[`botocore`] Doc only update - fixed broken
links.
-   api-change:`ec2`: \[`botocore`] Doc-only updates for Amazon EC2
- api-change:`events`: \[`botocore`] Amazon EventBridge adds support for
customer-managed keys on Archives and validations for two fields:
eventSourceArn and kmsKeyIdentifier.
- api-change:`s3control`: \[`botocore`] Updated max size of Prefixes
parameter of Scope data type.

###
[`v1.37.27`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13727)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.26...1.37.27)

\=======

- api-change:`bedrock-agent`: \[`botocore`] Added optional
"customMetadataField" for Amazon Aurora knowledge bases, allowing
single-column metadata. Also added optional "textIndexName" for MongoDB
Atlas knowledge bases, enabling hybrid search support.
- api-change:`chime-sdk-voice`: \[`botocore`] Added FOC date as an
attribute of PhoneNumberOrder, added AccessDeniedException as a possible
return type of ValidateE911Address
- api-change:`mailmanager`: \[`botocore`] Add support for Dual_Stack and
PrivateLink types of IngressPoint. For configuration requests, SES Mail
Manager will now accept both IPv4/IPv6 dual-stack endpoints and AWS
PrivateLink VPC endpoints for email receiving.
- api-change:`opensearch`: \[`botocore`] Improve descriptions for
various API commands and data types.
- api-change:`route53`: \[`botocore`] Added us-gov-east-1 and
us-gov-west-1 as valid Latency Based Routing regions for
change-resource-record-sets.
- api-change:`sagemaker`: \[`botocore`] Adds support for i3en, m7i, r7i
instance types for SageMaker Hyperpod
- api-change:`sesv2`: \[`botocore`] This release enables customers to
provide attachments in the SESv2 SendEmail and SendBulkEmail APIs.
- api-change:`transcribe`: \[`botocore`] This Feature Adds Support for
the "zh-HK" Locale for Batch Operations
- enhancement:Eventstream: \[`botocore`] The event streams maximum
payload size is now required to be 24Mb or less.

###
[`v1.37.26`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13726)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.25...1.37.26)

\=======

- api-change:`application-signals`: \[`botocore`] Application Signals
now supports creating Service Level Objectives on service dependencies.
Users can now create or update SLOs on discovered service dependencies
to monitor their standard application metrics.
- api-change:`codebuild`: \[`botocore`] This release adds support for
environment type WINDOWS_SERVER\_2022\_CONTAINER in ProjectEnvironment
- api-change:`ecr`: \[`botocore`] Fix for customer issues related to AWS
account ID and size limitation for token.
- api-change:`ecs`: \[`botocore`] This is an Amazon ECS documentation
only update to address various tickets.
- api-change:`lexv2-models`: \[`botocore`] Release feature of
errorlogging for lex bot, customer can config this feature in bot
version to generate log for error exception which helps debug
- api-change:`medialive`: \[`botocore`] Added support for SMPTE 2110
inputs when running a channel in a MediaLive Anywhere cluster. This
feature enables ingestion of SMPTE 2110-compliant video, audio, and
ancillary streams by reading SDP files that AWS Elemental MediaLive can
retrieve from a network source.

###
[`v1.37.25`](https://redirect.github.com/boto/boto3/blob/HEAD/CHANGELOG.rst#13725)

[Compare
Source](https://redirect.github.com/boto/boto3/compare/1.37.24...1.37.25)

\=======

- api-change:`cleanrooms`: \[`botocore`] This release adds support for
updating the analytics engine of a collaboration.
- api-change:`sagemaker`: \[`botocore`] Added tagging support for
SageMaker notebook instance lifecycle configurations

</details>

<details>
<summary>celery/celery (celery)</summary>

###
[`v5.5.1`](https://redirect.github.com/celery/celery/blob/HEAD/Changelog.rst#551)

[Compare
Source](https://redirect.github.com/celery/celery/compare/v5.5.0...v5.5.1)

\=====

:release-date: 2025-04-08
:release-by: Tomer Nosrati

What's Changed

```

- Fixed "AttributeError: list object has no attribute strip" with quorum queues and failover brokers (#&#8203;9657)
- Prepare for release: v5.5.1 (#&#8203;9660)

.. _version-5.5.0:
```

</details>

<details>
<summary>django/django (django)</summary>

###
[`v5.2`](https://redirect.github.com/django/django/compare/5.1.8...5.2)

[Compare
Source](https://redirect.github.com/django/django/compare/5.1.8...5.2)

</details>

<details>
<summary>django-extensions/django-extensions
(django-extensions)</summary>

###
[`v4.1`](https://redirect.github.com/django-extensions/django-extensions/blob/HEAD/CHANGELOG.md#41)

[Compare
Source](https://redirect.github.com/django-extensions/django-extensions/compare/4.0...4.1)

Changes:

- Add: show_permissions command
([#&#8203;1920](https://redirect.github.com/django-extensions/django-extensions/issues/1920))
- Improvement: graph_models, style per app
([#&#8203;1848](https://redirect.github.com/django-extensions/django-extensions/issues/1848))
- Fix: JSONField, bulk_update's
([#&#8203;1924](https://redirect.github.com/django-extensions/django-extensions/issues/1924))

###
[`v4.0`](https://redirect.github.com/django-extensions/django-extensions/blob/HEAD/CHANGELOG.md#40)

[Compare
Source](https://redirect.github.com/django-extensions/django-extensions/compare/3.2.3...4.0)

Changes:

-   Improvement: Support for Python 3.12 and 3.13
-   Improvement: Support for Django 5.x
-   Improvement: Switch from setup.{cfg,py} to pyproject.toml
- Improvement: graph_models, Add option to display field choices in
graph_models
([#&#8203;1854](https://redirect.github.com/django-extensions/django-extensions/issues/1854))
- Improvement: graph_models, Add webp support
([#&#8203;1857](https://redirect.github.com/django-extensions/django-extensions/issues/1857))
- Improvement: graph_models, Support for ordering edges on
pydot/dot/graphviz
([#&#8203;1914](https://redirect.github.com/django-extensions/django-extensions/issues/1914))
- Improvement: mail_debug, Update mail_debug command to use aiosmtpd
([#&#8203;1880](https://redirect.github.com/django-extensions/django-extensions/issues/1880))
- Improvement: shell_plus, Improve error message for missing import
([#&#8203;1898](https://redirect.github.com/django-extensions/django-extensions/issues/1898))
- Improvement: reset_db, Add reset_db support for django_tenants
([#&#8203;1855](https://redirect.github.com/django-extensions/django-extensions/issues/1855))
- Improvement: docs, various improvements
([#&#8203;1852](https://redirect.github.com/django-extensions/django-extensions/issues/1852),
[#&#8203;1888](https://redirect.github.com/django-extensions/django-extensions/issues/1888),
[#&#8203;1882](https://redirect.github.com/django-extensions/django-extensions/issues/1882),
[#&#8203;1901](https://redirect.github.com/django-extensions/django-extensions/issues/1901),
[#&#8203;1912](https://redirect.github.com/django-extensions/django-extensions/issues/1912),
[#&#8203;1913](https://redirect.github.com/django-extensions/django-extensions/issues/1913))
- Improvement: jobs, Handle non-package modules when looking for job
definitions
([#&#8203;1887](https://redirect.github.com/django-extensions/django-extensions/issues/1887))
- Improvement: Add django-prometheus DB backends support
([#&#8203;1800](https://redirect.github.com/django-extensions/django-extensions/issues/1800))
- Improvement: Call post_command when the command raises an unhandled
exception
([#&#8203;1837](https://redirect.github.com/django-extensions/django-extensions/issues/1837))
- Fix: sqldiff, do not consider ('serial', 'integer') nor ('bigserial',
'bigint') as a `field-type-differ`
([#&#8203;1867](https://redirect.github.com/django-extensions/django-extensions/issues/1867))
- Fix: shell_plus, Fix start up order and add history
([#&#8203;1869](https://redirect.github.com/django-extensions/django-extensions/issues/1869))
- Remove pipchecker and associated tests
([#&#8203;1906](https://redirect.github.com/django-extensions/django-extensions/issues/1906))
- Following Django's release numbering style more closely (see
https://docs.djangoproject.com/en/5.2/internals/release-process/ )

</details>

<details>
<summary>jschneier/django-storages (django-storages)</summary>

###
[`v1.14.6`](https://redirect.github.com/jschneier/django-storages/compare/1.14.5...1.14.6)

[Compare
Source](https://redirect.github.com/jschneier/django-storages/compare/1.14.5...1.14.6)

</details>

<details>
<summary>tfranzel/drf-spectacular-sidecar
(drf-spectacular-sidecar)</summary>

###
[`v2025.4.1`](https://redirect.github.com/tfranzel/drf-spectacular-sidecar/compare/2025.3.1...2025.4.1)

[Compare
Source](https://redirect.github.com/tfranzel/drf-spectacular-sidecar/compare/2025.3.1...2025.4.1)

</details>

<details>
<summary>ipython/ipython (ipython)</summary>

###
[`v9.1.0`](https://redirect.github.com/ipython/ipython/compare/9.0.2...9.1.0)

[Compare
Source](https://redirect.github.com/ipython/ipython/compare/9.0.2...9.1.0)

</details>

<details>
<summary>lxml/lxml (lxml)</summary>

###
[`v5.3.2`](https://redirect.github.com/lxml/lxml/blob/HEAD/CHANGES.txt#532-2025-04-05)

[Compare
Source](https://redirect.github.com/lxml/lxml/compare/lxml-5.3.1...lxml-5.3.2)

\==================

This release resolves CVE-2025-24928 as described in
https://gitlab.gnome.org/GNOME/libxml2/-/issues/847

## Bugs fixed

-   Binary wheels use libxml2 2.12.10 and libxslt 1.1.42.

- Binary wheels for Windows use a patched libxml2 2.11.9 and libxslt
1.1.39.

</details>

<details>
<summary>Python-Markdown/markdown (markdown)</summary>

###
[`v3.8`](https://redirect.github.com/Python-Markdown/markdown/releases/tag/3.8)

[Compare
Source](https://redirect.github.com/Python-Markdown/markdown/compare/3.7...3.8)

##### Changed

- DRY fix in `abbr` extension by introducing method `create_element`
([#&#8203;1483](https://redirect.github.com/Python-Markdown/markdown/issues/1483)).
-   Clean up test directory by removing some redundant tests and port
    non-redundant cases to the newer test framework.
- Improved performance of the raw HTML post-processor
([#&#8203;1510](https://redirect.github.com/Python-Markdown/markdown/issues/1510)).

##### Fixed

- Backslash Unescape IDs set via `attr_list` on `toc`
([#&#8203;1493](https://redirect.github.com/Python-Markdown/markdown/issues/1493)).
- Ensure `md_in_html` processes content inside "markdown" blocks as they
are
parsed outside of "markdown" blocks to keep things more consistent for
third-party extensions
([#&#8203;1503](https://redirect.github.com/Python-Markdown/markdown/issues/1503)).
- `md_in_html` handle tags within inline code blocks better
([#&#8203;1075](https://redirect.github.com/Python-Markdown/markdown/issues/1075)).
- `md_in_html` fix handling of one-liner block HTML handling
([#&#8203;1074](https://redirect.github.com/Python-Markdown/markdown/issues/1074)).
- Ensure `<center>` is treated like a block-level element
([#&#8203;1481](https://redirect.github.com/Python-Markdown/markdown/issues/1481)).
- Ensure that `abbr` extension respects `AtomicString` and does not
process
perceived abbreviations in these strings
([#&#8203;1512](https://redirect.github.com/Python-Markdown/markdown/issues/1512)).
- Ensure `smarty` extension correctly renders nested closing quotes
([#&#8203;1514](https://redirect.github.com/Python-Markdown/markdown/issues/1514)).

</details>

<details>
<summary>openai/openai-python (openai)</summary>

###
[`v1.73.0`](https://redirect.github.com/openai/openai-python/blob/HEAD/CHANGELOG.md#1730-2025-04-12)

[Compare
Source](https://redirect.github.com/openai/openai-python/compare/v1.72.0...v1.73.0)

Full Changelog:
[v1.72.0...v1.73.0](https://redirect.github.com/openai/openai-python/compare/v1.72.0...v1.73.0)

##### Features

- **api:** manual updates
([a3253dd](a3253dd798))

##### Bug Fixes

- **perf:** optimize some hot paths
([f79d39f](f79d39fbca))
- **perf:** skip traversing types for NotGiven values
([28d220d](28d220de3b))

##### Chores

- **internal:** expand CI branch coverage
([#&#8203;2295](https://redirect.github.com/openai/openai-python/issues/2295))
([0ae783b](0ae783b991))
- **internal:** reduce CI branch coverage
([2fb7d42](2fb7d425cd))
- slight wording improvement in README
([#&#8203;2291](https://redirect.github.com/openai/openai-python/issues/2291))
([e020759](e0207598d1))
- workaround build errors
([4e10c96](4e10c96a48))

###
[`v1.72.0`](https://redirect.github.com/openai/openai-python/blob/HEAD/CHANGELOG.md#1720-2025-04-08)

[Compare
Source](https://redirect.github.com/openai/openai-python/compare/v1.71.0...v1.72.0)

Full Changelog:
[v1.71.0...v1.72.0](https://redirect.github.com/openai/openai-python/compare/v1.71.0...v1.72.0)

##### Features

- **api:** Add evalapi to sdk
([#&#8203;2287](https://redirect.github.com/openai/openai-python/issues/2287))
([35262fc](35262fcef6))

##### Chores

- **internal:** fix examples
([#&#8203;2288](https://redirect.github.com/openai/openai-python/issues/2288))
([39defd6](39defd61e8))
- **internal:** skip broken test
([#&#8203;2289](https://redirect.github.com/openai/openai-python/issues/2289))
([e2c9bce](e2c9bce1f5))
- **internal:** slight transform perf improvement
([#&#8203;2284](https://redirect.github.com/openai/openai-python/issues/2284))
([746174f](746174fae7))
- **tests:** improve enum examples
([#&#8203;2286](https://redirect.github.com/openai/openai-python/issues/2286))
([c9dd81c](c9dd81ce02))

###
[`v1.71.0`](https://redirect.github.com/openai/openai-python/blob/HEAD/CHANGELOG.md#1710-2025-04-07)

[Compare
Source](https://redirect.github.com/openai/openai-python/compare/v1.70.0...v1.71.0)

Full Changelog:
[v1.70.0...v1.71.0](https://redirect.github.com/openai/openai-python/compare/v1.70.0...v1.71.0)

##### Features

- **api:** manual updates
([bf8b4b6](bf8b4b6990))
- **api:** manual updates
([3e37aa3](3e37aa3e15))
- **api:** manual updates
([dba9b65](dba9b656fa))
- **api:** manual updates
([f0c463b](f0c463b478))

##### Chores

- **deps:** allow websockets v15
([#&#8203;2281](https://redirect.github.com/openai/openai-python/issues/2281))
([19c619e](19c619ea95))
- **internal:** only run examples workflow in main repo
([#&#8203;2282](https://redirect.github.com/openai/openai-python/issues/2282))
([c3e0927](c3e0927d3f))
- **internal:** remove trailing character
([#&#8203;2277](https://redirect.github.com/openai/openai-python/issues/2277))
([5a21a2d](5a21a2d799))
- Remove deprecated/unused remote spec feature
([23f76eb](23f76eb0b9))

</details>

<details>
<summary>jupyter-server/pycrdt (pycrdt)</summary>

###
[`v0.12.12`](https://redirect.github.com/jupyter-server/pycrdt/blob/HEAD/CHANGELOG.md#01212)

[Compare
Source](https://redirect.github.com/jupyter-server/pycrdt/compare/0.12.11...0.12.12)

-   Add doc and shared type `events()` async event iterator.
-   Fix deadlock while getting root type from within transaction.

###
[`v0.12.11`](https://redirect.github.com/jupyter-server/pycrdt/blob/HEAD/CHANGELOG.md#01211)

[Compare
Source](https://redirect.github.com/jupyter-server/pycrdt/compare/0.12.10...0.12.11)

-   Upgrade `pyo3` to v0.24.1.

</details>

<details>
<summary>pytest-dev/pytest-cov (pytest-cov)</summary>

###
[`v6.1.1`](https://redirect.github.com/pytest-dev/pytest-cov/blob/HEAD/CHANGELOG.rst#611-2025-04-05)

[Compare
Source](https://redirect.github.com/pytest-dev/pytest-cov/compare/v6.1.0...v6.1.1)

- Fixed breakage that occurs when `--cov-context` and the `no_cover`
marker are used together.

###
[`v6.1.0`](https://redirect.github.com/pytest-dev/pytest-cov/blob/HEAD/CHANGELOG.rst#610-2025-04-01)

[Compare
Source](https://redirect.github.com/pytest-dev/pytest-cov/compare/v6.0.0...v6.1.0)

- Change terminal output to use full width lines for the coverage
header.
Contributed by Tsvika Shapira in `#&#8203;678
<https://github.com/pytest-dev/pytest-cov/pull/678>`\_.
- Removed unnecessary CovFailUnderWarning. Fixes `#&#8203;675
<https://github.com/pytest-dev/pytest-cov/issues/675>`\_.
- Fixed the term report not using the precision specified via
`--cov-precision`.

</details>

<details>
<summary>pytest-dev/pytest-django (pytest-django)</summary>

###
[`v4.11.1`](https://redirect.github.com/pytest-dev/pytest-django/releases/tag/v4.11.1)

[Compare
Source](https://redirect.github.com/pytest-dev/pytest-django/compare/v4.11.0...v4.11.1)


https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst#v4111-2025-04-03

###
[`v4.11.0`](https://redirect.github.com/pytest-dev/pytest-django/releases/tag/v4.11.0)

[Compare
Source](https://redirect.github.com/pytest-dev/pytest-django/compare/v4.10.0...v4.11.0)


https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst#v4110-2025-04-01

</details>

<details>
<summary>astral-sh/ruff (ruff)</summary>

###
[`v0.11.5`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0115)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.11.4...0.11.5)

##### Preview features

- \[`airflow`] Add missing `AIR302` attribute check
([#&#8203;17115](https://redirect.github.com/astral-sh/ruff/pull/17115))
- \[`airflow`] Expand module path check to individual symbols (`AIR302`)
([#&#8203;17278](https://redirect.github.com/astral-sh/ruff/pull/17278))
- \[`airflow`] Extract `AIR312` from `AIR302` rules (`AIR302`, `AIR312`)
([#&#8203;17152](https://redirect.github.com/astral-sh/ruff/pull/17152))
- \[`airflow`] Update oudated `AIR301`, `AIR302` rules
([#&#8203;17123](https://redirect.github.com/astral-sh/ruff/pull/17123))
- \[syntax-errors] Async comprehension in sync comprehension
([#&#8203;17177](https://redirect.github.com/astral-sh/ruff/pull/17177))
- \[syntax-errors] Check annotations in annotated assignments
([#&#8203;17283](https://redirect.github.com/astral-sh/ruff/pull/17283))
- \[syntax-errors] Extend annotation checks to `await`
([#&#8203;17282](https://redirect.github.com/astral-sh/ruff/pull/17282))

##### Bug fixes

- \[`flake8-pie`] Avoid false positive for multiple assignment with
`auto()` (`PIE796`)
([#&#8203;17274](https://redirect.github.com/astral-sh/ruff/pull/17274))

##### Rule changes

- \[`ruff`] Fix `RUF100` to detect unused file-level `noqa` directives
with specific codes
([#&#8203;17042](https://redirect.github.com/astral-sh/ruff/issues/17042))
([#&#8203;17061](https://redirect.github.com/astral-sh/ruff/pull/17061))
- \[`flake8-pytest-style`] Avoid false positive for legacy form of
`pytest.raises` (`PT011`)
([#&#8203;17231](https://redirect.github.com/astral-sh/ruff/pull/17231))

##### Documentation

- Fix formatting of "See Style Guide" link
([#&#8203;17272](https://redirect.github.com/astral-sh/ruff/pull/17272))

###
[`v0.11.4`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0114)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.11.3...0.11.4)

##### Preview features

- \[`ruff`] Implement `invalid-rule-code` as `RUF102`
([#&#8203;17138](https://redirect.github.com/astral-sh/ruff/pull/17138))
- \[syntax-errors] Detect duplicate keys in `match` mapping patterns
([#&#8203;17129](https://redirect.github.com/astral-sh/ruff/pull/17129))
- \[syntax-errors] Detect duplicate attributes in `match` class patterns
([#&#8203;17186](https://redirect.github.com/astral-sh/ruff/pull/17186))
- \[syntax-errors] Detect invalid syntax in annotations
([#&#8203;17101](https://redirect.github.com/astral-sh/ruff/pull/17101))

##### Bug fixes

- \[syntax-errors] Fix multiple assignment error for class fields in
`match` patterns
([#&#8203;17184](https://redirect.github.com/astral-sh/ruff/pull/17184))
- Don't skip visiting non-tuple slice in `typing.Annotated` subscripts
([#&#8203;17201](https://redirect.github.com/astral-sh/ruff/pull/17201))

###
[`v0.11.3`](https://redirect.github.com/astral-sh/ruff/blob/HEAD/CHANGELOG.md#0113)

[Compare
Source](https://redirect.github.com/astral-sh/ruff/compare/0.11.2...0.11.3)

##### Preview features

- \[`airflow`] Add more autofixes for `AIR302`
([#&#8203;16876](https://redirect.github.com/astral-sh/ruff/pull/16876),
[#&#8203;16977](https://redirect.github.com/astral-sh/ruff/pull/16977),
[#&#8203;16976](https://redirect.github.com/astral-sh/ruff/pull/16976),
[#&#8203;16965](https://redirect.github.com/astral-sh/ruff/pull/16965))
- \[`airflow`] Move `AIR301` to `AIR002`
([#&#8203;16978](https://redirect.github.com/astral-sh/ruff/pull/16978))
- \[`airflow`] Move `AIR302` to `AIR301` and `AIR303` to `AIR302`
([#&#8203;17151](https://redirect.github.com/astral-sh/ruff/pull/17151))
- \[`flake8-bandit`] Mark `str` and `list[str]` literals as trusted
input (`S603`)
([#&#8203;17136](https://redirect.github.com/astral-sh/ruff/pull/17136))
- \[`ruff`] Support slices in `RUF005`
([#&#8203;17078](https://redirect.github.com/astral-sh/ruff/pull/17078))
- \[syntax-errors] Start detecting compile-time syntax errors
([#&#8203;16106](https://redirect.github.com/astral-sh/ruff/pull/16106))
- \[syntax-errors] Duplicate type parameter names
([#&#8203;16858](https://redirect.github.com/astral-sh/ruff/pull/16858))
- \[syntax-errors] Irrefutable `case` pattern before final case
([#&#8203;16905](https://redirect.github.com/astral-sh/ruff/pull/16905))
- \[syntax-errors] Multiple assignments in `case` pattern
([#&#8203;16957](https://redirect.github.com/astral-sh/ruff/pull/16957))
- \[syntax-errors] Single starred assignment target
([#&#8203;17024](https://redirect.github.com/astral-sh/ruff/pull/17024))
- \[syntax-errors] Starred expressions in `return`, `yield`, and `for`
([#&#8203;17134](https://redirect.github.com/astral-sh/ruff/pull/17134))
- \[syntax-errors] Store to or delete `__debug__`
([#&#8203;16984](https://redirect.github.com/astral-sh/ruff/pull/16984))

##### Bug fixes

- Error instead of `panic!` when running Ruff from a deleted directory
([#&#8203;16903](https://redirect.github.com/astral-sh/ruff/issues/16903))
([#&#8203;17054](https://redirect.github.com/astral-sh/ruff/pull/17054))
- \[syntax-errors] Fix false positive for parenthesized tuple index
([#&#8203;16948](https://redirect.github.com/astral-sh/ruff/pull/16948))

##### CLI

- Check `pyproject.toml` correctly when it is passed via stdin
([#&#8203;16971](https://redirect.github.com/astral-sh/ruff/pull/16971))

##### Configuration

- \[`flake8-import-conventions`] Add import `numpy.typing as npt` to
default `flake8-import-conventions.aliases`
([#&#8203;17133](https://redirect.github.com/astral-sh/ruff/pull/17133))

##### Documentation

- \[`refurb`] Document why `UserDict`, `UserList`, and `UserString` are
preferred over `dict`, `list`, and `str` (`FURB189`)
([#&#8203;16927](https://redirect.github.com/astral-sh/ruff/pull/16927))

</details>

<details>
<summary>getsentry/sentry-python (sentry-sdk)</summary>

###
[`v2.25.1`](https://redirect.github.com/getsentry/sentry-python/blob/HEAD/CHANGELOG.md#2251)

[Compare
Source](https://redirect.github.com/getsentry/sentry-python/compare/2.25.0...2.25.1)

##### Various fixes & improvements

- fix(logs): Add a class which batches groups of logs together.
([#&#8203;4229](https://redirect.github.com/getsentry/sentry-python/issues/4229))
by [@&#8203;colin-sentry](https://redirect.github.com/colin-sentry)
- fix(logs): Use repr instead of json for message and arguments
([#&#8203;4227](https://redirect.github.com/getsentry/sentry-python/issues/4227))
by [@&#8203;colin-sentry](https://redirect.github.com/colin-sentry)
- fix(logs): Debug output from Sentry logs should always be `debug`
level.
([#&#8203;4224](https://redirect.github.com/getsentry/sentry-python/issues/4224))
by [@&#8203;antonpirker](https://redirect.github.com/antonpirker)
- fix(ai): Do not consume anthropic streaming stop
([#&#8203;4232](https://redirect.github.com/getsentry/sentry-python/issues/4232))
by [@&#8203;colin-sentry](https://redirect.github.com/colin-sentry)
- fix(spotlight): Do not spam sentry_sdk.warnings logger w/ Spotlight
([#&#8203;4219](https://redirect.github.com/getsentry/sentry-python/issues/4219))
by [@&#8203;BYK](https://redirect.github.com/BYK)
- fix(docs): fixed code snippet
([#&#8203;4218](https://redirect.github.com/getsentry/sentry-python/issues/4218))
by [@&#8203;antonpirker](https://redirect.github.com/antonpirker)
- build(deps): bump actions/create-github-app-token from 1.11.7 to
1.12.0
([#&#8203;4214](https://redirect.github.com/getsentry/sentry-python/issues/4214))
by [@&#8203;dependabot](https://redirect.github.com/dependabot)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 7am on monday" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/suitenumerique/docs).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMjcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiLCJub0NoYW5nZUxvZyJdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Manuel Raynaud <manu@raynaud.io>
2025-04-28 14:05:52 +02:00
Tom Hubrecht
5cdbdbf215 (settings) Allow configuring PKCE for the SSO (#886)
C.f.
https://mozilla-django-oidc.readthedocs.io/en/latest/settings.html#OIDC_USE_PKCE

## Purpose

Add pkce settings

## Proposal
Get the settings from the environment

Signed-off-by: Tom Hubrecht <github@mail.hubrecht.ovh>
2025-04-28 12:54:30 +02:00
Anthony LC
5268699d50 ⬆️(dependencies) update js dependencies 2025-04-23 11:43:50 +02:00
virgile-dev
cdafe6fd33 📝(readme) update xl packages info (#885)
Info message so people fulfill their licencing obligations

Signed-off-by: virgile-deville <virgile.deville@beta.gouv.fr>
2025-04-22 13:57:45 +00:00
Anthony LC
4307b4f433 🐛(backend) race condition create doc
When 2 docs are created almost at the same time,
the second one will fail because the first one.
We get a unicity error on the path key already
used ("impress_document_path_key").
To fix this issue, we will lock the table the
time to create the document, the next query will
wait for the lock to be released.
2025-04-22 11:43:29 +02:00
Anthony LC
3bf33d202a ️(frontend) reduce unblocking time for config
We will serve the config from the cache if available
in waiting for the config to be loaded.
It will remove the loading time for the config except
when the config is not available in the cache.
2025-04-22 11:23:55 +02:00
Anthony LC
101cef7d70 ♻️(frontend) refacto useCunninghamTheme
Refacto useCunninghamTheme, we don't need a function
to have access to the tokens anymore.
2025-04-22 10:38:51 +02:00
Samuel Paccoud - DINUM
419079ac69 🚸(backend) make document search on title accent-insensitive
This should work in both cases:
- search for "vélo" when the document title contains "velo"
- search for "velo" when the document title contains "vélo"
2025-04-17 20:28:14 +02:00
Anthony LC
ecd06560c6 🚚(frontend) Display homepage on /home url
The homepage is now accessible at the /home URL.
Before the homepage was accessible on the /login URL.
We still keep the /login URL for backward compatibility.
2025-04-13 13:25:40 +02:00
Anthony LC
e9ab099ce0 🚩(frontend) integrate homepage feature flag
If the homepage feature flag is enabled,
the homepage will be displayed.
2025-04-13 13:25:40 +02:00
Anthony LC
67b69d05f7 🚩(backend) add homepage feature flag
Add a homepage feature flag that we will
propagate to the frontend.
It will be used to enable or disable the
homepage at runtime.
2025-04-13 13:25:40 +02:00
virgile-deville
f429eb053a 📝(readme) remove preprod account
Having a preprod account using a yopmail account was a security bad practice

Co-authored-by: Samuel Paccoud <sampaccoud@users.noreply.github.com>
2025-04-11 22:29:22 +02:00
Olivier Laurendeau
ad11b7f554 📝(docs) init architecture documentation
- Add docs about architecture
- Add ADR about the CRDT choice
2025-04-10 15:15:15 +02:00
Anthony LC
3d5adad227 🔖(minor) release 3.1.0
Added:
- 🚩(backend) add feature flag for the footer
- 🔧(backend) add view to manage footer json
- (frontend) add custom css style
- 🚩(frontend) conditionally render AI button only
  when feature is enabled

Changed:
- 🚨(frontend) block button when creating doc

Fixed:
- 🐛(back) validate document content in serializer
- 🐛(frontend) fix selection click
  past end of content
2025-04-08 12:41:38 +02:00
Anthony LC
de8e812f2f 🥚(frontend) remove easter egg
Remove the easter egg from the console.
2025-04-07 13:18:04 +02:00
Nathan Panchout
7a1601c682 (frontend) update favicon files and links
- Added new favicon files: favicon-dark.png and favicon.png.
- Updated the _app.tsx file to link to the new favicon files, supporting
both light and dark color schemes.
2025-04-07 13:18:04 +02:00
Nathan Panchout
0537572542 ♻️(frontend) icon component refactoring
- Add variant to IconComponent and remove $isMaterialIcon prop
- Replace all Text component used as icon with the Icon component.
2025-04-07 13:18:04 +02:00
Anthony LC
8aab007ad1 🐛(frontend) do not display firefox modal if not necessary
It is necessary to display the firefox modal only
if the user has something to save.
2025-04-07 13:18:04 +02:00
Anthony LC
cde3de43f7 ♻️(frontend) misc improvements
- add opacity props on Box
- rename to cunningham-style.css
- update illustration-docs-empty.png
- smaller tooltip
2025-04-07 13:18:04 +02:00
Anthony LC
8c0c3c2f44 🐛(frontend) fix selection click past end of content
On Chrome, when we click at the end of a line,
the cursor is placed at the beginning of the line.
We fix this behavior, now the cursor is placed
at the end of the line.
2025-04-07 13:18:04 +02:00
Anthony LC
c11d59c434 🚩(backend) add feature flag for the footer
We added the feature flag `FRONTEND_FOOTER_FEATURE_ENABLED`
to enable or disable the footer in the frontend.
2025-04-04 15:44:38 +02:00
Manuel Raynaud
8836109945 ♻️(back) reset cache after every test
We move the cache reset in the global conf test to not have to think
about reseting the cache when we implement test.
2025-04-04 15:44:38 +02:00
Anthony LC
ba136ff82f 🔧(backend) add view to manage footer json
We added the `FRONTEND_URL_JSON_FOOTER` environment
variable. It will give the possibility to generate
your own footer content in the frontend.
If the variable is not set, the footer will not
be displayed.
2025-04-04 15:44:38 +02:00
Anthony LC
96d9d1a184 🔊(y-provider) improve and add logs
We have somes entries with "No cookies", we
add more logs to understand why we have this case.
We add the datetime in front of each entries as
well.
2025-04-03 16:27:40 +02:00
Manuel Raynaud
771ffdc7cc 🔥(y-provider) remove npm in docker image
We use yarn and not npm, we remove npm because it has a dependencie with
cross-spawn which has a CVE.
2025-04-03 10:41:55 +02:00
Manuel Raynaud
82eba1e8ea 🔥(ci) force ci to fails if trivy fails
If trivy fails we must stop the CI to avoid publishing images with
security issues.
2025-04-03 10:41:55 +02:00
renovate[bot]
8c42599d0f ⬆️(dependencies) update django to v5.1.8 [SECURITY] 2025-04-03 10:28:12 +02:00
renovate[bot]
8620cf4857 ⬆️(dependencies) update next to v15.2.4 [SECURITY] 2025-04-03 07:02:20 +02:00
Baptiste Fontaine
2a7da73248 📝(docs) fix grammar and typos
Fix grammar and typos in docs/installation.md file.
2025-04-02 11:53:56 +02:00
Manuel Raynaud
e8e9922832 (back) remove url-normalize dependency
We have the dependency url-normalize installed but we don't use it in
our codebase.
2025-04-01 09:51:29 +02:00
renovate[bot]
2da4ce4570 ⬆️(dependencies) update python dependencies 2025-04-01 09:51:29 +02:00
Anthony LC
50b90f9ae7 (e2e) fix some flaky tests
Some tests were flaky, causing them to fail
intermittently. This commit aims to address
this issue.
2025-03-31 12:34:04 +02:00
Anthony LC
65ddf7fbe8 📝(docs) add documentation about runtime theming
Add a documentation page about runtime theming.
It explains how to use the theming system
and provide a example.
2025-03-31 12:34:04 +02:00
Anthony LC
d3a7ee74b3 💄(frontend) add classname to components
To be easily customized, we added a classname
to most of the components.
2025-03-31 12:34:04 +02:00
Anthony LC
65e450c6cc (frontend) add custom css style
From the config, we can add custom css style
to the app.
2025-03-31 12:34:04 +02:00
Anthony LC
725cae5470 🔧(backend) add FRONTEND_CSS_URL env var
We added the `FRONTEND_CSS_URL` environment
variable. It will give the possibility to add a
css layer at runtime.
2025-03-31 12:34:04 +02:00
Anthony LC
3881930e82 🚨(frontend) block button when creating doc
When the user clicks on the button to create a new doc,
the button is disabled to prevent multiple clicks.
Multiple clicks on the button could create multiple docs
and create a error about duplicated paths.
2025-03-31 11:29:28 +02:00
Anthony LC
910686293c ️(frontend) improve heading store
Reset headings only when the headers are not
equal to the previous ones. It will prevent
unnecessary rerenders.
2025-03-31 11:29:28 +02:00
Anthony LC
7e7c9ac4c5 ♻️(frontend) replace useModal hook
useModal hook does not use useCallback for its
methods that creates useless rerenders.
2025-03-31 11:29:28 +02:00
Anthony LC
d5d2cfab8e ♻️(frontend) improve useSaveDoc hook
- add tests for useSaveDoc hook
- simplify hooks states
- reduce useEffect calls
2025-03-31 11:29:28 +02:00
Matthias
f2ed8e0ea1 🐛(frontend) conditionally render AI button in toolbar
Added a feature flag check to ensure the AIGroupButton is only rendered
when AI_FEATURE_ENABLED is explicitly set to "true". This prevents the
AI button from appearing when the feature is not configured or disabled.

Fixes #782

Signed-off-by: Matthias <matthias@universum.com>
2025-03-31 11:04:00 +02:00
Manuel Raynaud
fbe8a26dba 🐛(back) validate document content in serializer
We recently extract images url in the content. For this, we assume that
the document content is always in base64. We enforce this assumption by
checking if it's a valide base64 in the serializer.
2025-03-29 19:08:39 +01:00
Berry den Hartog
3e974be9f4 📝(docs) describe environmental options for docs backend (#821)
Signed-off-by: 
Berry den Hartog <38954346+berrydenhartog@users.noreply.github.com>
2025-03-28 17:15:35 +00:00
Bastien Guerry
10f9d25920 📄(legal) add warning about legal compliance
We need to double-check our legal constraints regarding the use of XL
packages within Docs. In the meantime, sends a message to potential
reusers.
2025-03-28 17:15:16 +01:00
Jacques ROUSSEL
4178693e63 🐛(ci) use github action for argocd webhook notification
In order to refactor this notification between alls projetcs, we choose
to use a custom github action
2025-03-28 16:42:45 +01:00
Anthony LC
53be6de5f8 🔖(major) release 3.0.0
Added:
- 📄(legal) Require contributors to sign a DCO

Changed:
- ♻️(frontend) Integrate UI kit
- 🏗️(y-provider) manage auth in y-provider app

Fixed:
- 🐛(backend) compute ancestor_links in get_abilities
  if needed
- 🔒️(back) restrict access to document accesses
2025-03-28 15:32:08 +01:00
Anthony LC
4ff90abdee 🐛(service-worker) force reload new service worker
When multiple tabs are open, the new service worker
can stay in the "waiting" state and not be activated
until the other tabs with the old service worker
are closed.
We fix this by forcing the other tabs to reload
the page when a new service worker is detected.
All tabs will then be reloaded and the new service
worker will be activated.
2025-03-28 15:32:08 +01:00
Anthony LC
544dd00c16 🔧(helm) adapt setting helm dev file
The way that collaboration server authentifies the user
has changed. We adapt the configuration to the new
way of doing it, by removing the nginx auth url,
and by adding COLLABORATION_BACKEND_BASE_URL
setting.
2025-03-28 15:32:08 +01:00
Anthony LC
a3cd4c51ea 🩹(frontend) fine tunning for v3.0.0
- fix width select export
2025-03-28 15:32:08 +01:00
Manuel Raynaud
7e1eed3abd (y-provider) check hocuspocus documentName validity
We only use uuid v4 as hocuspocus dicument name. To be sure nothing else
is used we check that the documentName is a valid uuid version 4.
2025-03-27 18:42:04 +01:00
Manuel Raynaud
8bee476b5b 🔥(back) remove collaboration-auth endpoint
We don't need anymore the collaboration-auth endpoint. Every code
related to it is removed.
2025-03-27 18:42:04 +01:00
Manuel Raynaud
e86919fb9a 🏗️(y-provider) manage auth in y-provider app
The way to connect to the hocuspocus server needs to be proxified in
nginx to query a dedicated route in the django application and then
follow the request to the express server with the additionnal headers.
The auth can be done in the express server by querying the backend on
the document retrieve endpoint. If the response status code is 200, the
user has access to the document, otherwise it is not the case. Then we
can check the abilities to determine what the user can do or not.
2025-03-27 18:42:04 +01:00
Manuel Raynaud
a5b9169eb6 ♻️(back) replace Ypy by pycrdt
Ypy is deprecated and unmaintained. We have problem with parsing
existing documents. We replace it by pycrdt, library actively maintained
and without the issues we have with Ypy.
2025-03-27 18:27:04 +01:00
Manuel Raynaud
c0dfb4b6b3 ♻️(back) remove filtering on logging handler
Level filtering was used on the logging console handler. We remove as it
is not necessary to have it.
2025-03-27 18:27:04 +01:00
Manuel Raynaud
be051ad7d2 🐛(ci) use sha256 to sign argocd webhook call
The argocd webhook call needs now to use sha256 digest now to sign
2025-03-27 18:27:04 +01:00
Manuel Raynaud
a4452784e1 🔒️(back) restrict accesss to document accesses
Every user having an access to a document, no matter its role have
access to the entire accesses list with all the user details. Only
owner or admin should be able to have the entire list, for the other
roles, they have access to the list containing only owner and
administrator with less information on the username. The email and its
id is removed
2025-03-26 10:40:53 +01:00
Quentin BEY
2929e98260 ♻️(documents) inherit manager from queryset
During a code review, I saw we are overriding the MP_NodeManager and
redefine the queryset filters:

- The MP_NodeManager sorts the queryset by `path` by default and it's
  not done on our side, is it on purpose?
- The fact we need to redefine `readable_per_se` as a boilerplate is
  surprising.

I suggest we use the Django mechanism to generate the manager from the
queryset.
2025-03-24 15:04:50 +01:00
Manuel Raynaud
a1914c6259 🐛(backend) compute ancestor_links in get_abilities if needed
The refactor made in the tree view caching the ancestors_links to not
compute them again in the document.get_abilities method lead to a bug.
If the get_abilities method is called without ancestors_links, then they
are computed on all the ancestors but not from the highest readable
ancestor for the current user. We have to compute them with this
constraint.
2025-03-24 14:04:46 +01:00
Samuel Paccoud - DINUM
c882f1386c ♻️(backend) remove lazy from languages field on User model
The idea behind wrapping choices in `lazy` function was to allow
overriding the list of languages in tests with `override_settings`.
This was causin makemigrations to keep on including the field in
migrations when it is not needed. Since we finally don't override
the LANGUAGES setting in tests, we can remove it to fix the problem.
2025-03-24 10:43:45 +01:00
Samuel Paccoud - DINUM
c02f19a2cd (backend) extract attachment keys from updated content for access
We can't prevent document editors from copy/pasting content to from one
document to another. The problem is that copying content, will copy the
urls pointing to attachments but if we don't do anything, the reader of
the document to which the content is being pasted, may not be allowed to
access the attachment files from the original document.

Using the work from the previous commit, we can grant access to the readers
of the target document by extracting the attachment keys from the content and
adding themto the target document's "attachments" field. Before doing this,
we check that the current user can indeed access the attachment files extracted
from the content and that they are allowed to edit the current document.
2025-03-24 10:43:45 +01:00
Samuel Paccoud - DINUM
34a208a80d (backend) add duplicate action to the document API endpoint
We took this opportunity to refactor the way access is controlled on
media attachments. We now add the media key to a list on the document
instance each time a media is uploaded to a document. This list is
passed along when a document is duplicated, allowing us to grant
access to readers on the new document, even if they don't have or
lost access to the original document.

We also propose an option to reproduce the same access rights on the
duplicate document as what was in place on the original document.
This can be requested by passing the "with_accesses=true" option in
the query string.

The tricky point is that we need to extract attachment keys from the
existing documents and set them on the new "attachments" field that is
now used to track access rights on media files.
2025-03-24 10:43:45 +01:00
Samuel Paccoud - DINUM
6976bb7c78 (backend) fix migration test using model factory
Migration tests should not import and use factories or models
directly from the code because they would not be in sync with
the database in the state that each state needs to test it.

Instead the migrator object passed as argument allows us to
retrieve a minimal version of the models in sync with the state
of the database that we are testing. What we get is a minimal
model and we need to simulate all the methods that we could have
on the real model and that are needed for testing.
2025-03-24 10:43:45 +01:00
Samuel Paccoud - DINUM
621393165f (backend) add missing test on media-auth and collaboration-auth
These methods were involved in a bug that was fixed without first
evidencing the error in a test:
https://github.com/suitenumerique/docs/pull/556

Fixes https://github.com/suitenumerique/docs/issues/567
2025-03-24 10:43:45 +01:00
Samuel Paccoud - DINUM
3e9b530985 (backend) add missing tests for collaboration auth
Tests were forgotten. While writing the tests, I fixed
a few edge cases like the possibility to connect to the
collaboration server for an anonymous user.
2025-03-24 10:43:45 +01:00
Samuel Paccoud - DINUM
54f9b3963e ♻️(backend) refactor media_auth and collaboration_auth for flexibility
These 2 actions had factorized code but a few iterations lead to
spaghetti code where factorized code includes "if" clauses.

Refactor abstractions so that code factorization really works.
2025-03-24 10:43:45 +01:00
Samuel Paccoud - DINUM
710bbf512c (backend) add util to extract text from Ydoc content
Documents content is stored in the Ydoc format. We need a util
to extract it as xml/text.
2025-03-24 10:43:45 +01:00
Jacques ROUSSEL
747ca70186 🐛(ci) fix Tilt resources dependencies
The Tilt stack was not starting properly due to dependency issues. We
need to wait for PostgreSQL to be running before starting the migration.
2025-03-24 09:33:15 +01:00
renovate[bot]
9374495fda ⬆️(dependencies) update next to v15.2.3 [SECURITY] 2025-03-24 09:18:33 +01:00
Bastien Guerry
ef7cc67387 📄(legal) Require contributors to sign a DCO
Contributors are required to sign off their commits: this confirms
that they have read and accepted https://developercertificate.org.
2025-03-23 09:57:35 +01:00
Sylvain Zimmer
a8529e434a 🐛(media) fix compatibility with Scaleway Object Storage
Some providers with S3-compatible APIs have slightly different
implementations. In this case, Scaleway didn't accept version_id=""
and has a different version ID scheme. This was tested successfully
and should remain compatible with any other provider.
2025-03-22 18:00:43 +01:00
Manuel Raynaud
f8203a1766 🚨(back) lint code with ruff 0.11.2
New Ruff rule (C420) detects code that should be linted. We apply this
new rule on our code.
2025-03-22 10:28:48 +01:00
renovate[bot]
ce8b98e256 ⬆️(dependencies) update python dependencies 2025-03-22 10:28:48 +01:00
Anthony LC
4243519eee 🔥(frontend) remove Marianne font
Marianne font is now part of the UI kit.
We can remove it from the project.
2025-03-21 17:49:06 +01:00
Nathan Panchout
1abf529891 (frontend) refactor and theme token update
The configuration file has been simplified by importing configurations
from @gouvfr-lasuite/ui-kit . Colors and components have been updated to
reflect the new values. Additionally, adjustments have been made to
global styles, including the addition of styles for Material icons. Form
components have also been modified to incorporate the new style
properties.
2025-03-21 17:49:06 +01:00
Nathan Panchout
69ca4af539 (frontend) updated dependencies and added new packages
Added several new dependencies to the `package.json` file, including
`@dnd-kit/core`, `@dnd-kit/modifiers`, `@fontsource/material-icons`, and
`@gouvfr-lasuite/ui-kit`.
2025-03-21 17:49:06 +01:00
Anthony LC
14b2adedfb 🔖(minor) release 2.6.0
Added:
- 📝(doc) add publiccode.yml

Changed
- 🚸(frontend) ctrl+k modal not when editor is focused

Fixed:
- 🐛(back) allow only images to be used with
  the cors-proxy
- 🐛(backend) stop returning inactive users
  on the list endpoint
- 🔒️(backend) require at least 5 characters
  to search for users
- 🔒️(back) throttle user list endpoint
- 🔒️(back) remove pagination and limit to
   5 for user list endpoint
2025-03-21 17:07:26 +01:00
Anthony LC
a7edb382a7 🩹(frontent) change selector to block cmd+k
Multiple ctrl+k could open the search modal, we
change the selector, now if the toolbar is displayed
we don't open the search modal.
2025-03-21 17:07:26 +01:00
Anthony LC
fb5400c26b ️(frontend) search users with at least 5 characters
We now only search for users when the query
is at least 5 characters long.
2025-03-21 15:44:09 +01:00
Manuel Raynaud
8473facbee 🔒️(back) throttle user list endpoint
The user list endpoint is throttle to avoid users discovery. The
throttle is set to 500 requests per day. This can be changed using the
settings API_USERS_LIST_THROTTLE_RATE.
2025-03-21 15:44:09 +01:00
Anthony LC
5db446e8a8 🏷️(frontend) adapt type for user search
The response from the user request is now an
array of users, we don't paginate anymore.
We adapt the types to reflect this.
2025-03-21 15:44:09 +01:00
Manuel Raynaud
34dfb3fd66 🔒️(back) remove pagination and limit to 5 for user list endpoint
The user list endpoint does not use anymore a pagination, the results is
directly return in a list and the max results returned is limited to 5.
In order to modify this limit the settings API_USERS_LIST_LIMIT is
used.
2025-03-21 15:44:09 +01:00
Samuel Paccoud - DINUM
f9a91eda2d 🐛(backend) stop returning inactive users on the list endpoint
inactive users should not be returned as we don't want users to be
able to share new documents with them.
2025-03-21 15:44:09 +01:00
Samuel Paccoud - DINUM
eba926dea4 🔒️(backend) require at least 5 characters to search for users
Listing users is made a little to easy for authenticated users.
2025-03-21 15:44:09 +01:00
Anthony LC
3839a2e8b1 💄(frontend) improve contrast of Beta icon
The colors of the Beta icon were not contrasted
enough. This was posing an accessibility issue.
We now use a more contrasted color.
2025-03-21 09:22:42 +01:00
Anthony LC
a88d62e07d 🌐(frontend) make Docs title translatable
The title of the docs page was not translatable.
We now use the `t` function to translate the title.
2025-03-21 09:22:42 +01:00
Paul Mustière
b61a7a4961 📝(docs) fix typo
Correct language to not be past tense
2025-03-21 06:38:27 +01:00
Anthony LC
20d32ecc4e 🚸(frontend) ctrl+k modal not when editor is focused
ctrl+k interaction was as well used in the editor.
So if the user has a focus on the editor, we don't
open the searchmodal.
2025-03-20 17:43:32 +01:00
Manuel Raynaud
313acf4f78 🐛(back) allow only images to be used with the cors-proxy
The cors-proxy endpoint allowed to use every type of files and to
execute it in the browser. We limit the scope only to images and
Content-Security-Policy and Content-Disposition headers are also added
to not allow script execution that can be present in a SVG file.
2025-03-20 16:10:47 +01:00
Bastien
3a6105cc7e 📝(doc) add publiccode.yml (#770)
publiccode.yml is a standard for describing Free Software projects,
similar to other initiatives such as https://codemeta.github.io.

It is particularly suitable for describing projects funded by public
administrations. See https://github.com/publiccodeyml/publiccode.yml
2025-03-19 21:28:32 +01:00
Anthony LC
bbe17156be 🔖(minor) release 2.5.0
Added:
- 📝(doc) Added GNU Make link to README
- (frontend) add pinning on doc detail
- 🚩(frontend) feature flag analytic on copy as html
- (frontend) Custom block divider with export
- 🌐(i18n) activate dutch language

Changed:
- 🧑‍💻(frontend) change literal section open source
- ♻️(frontend) replace cors proxy for export
- 🚨(gitlint) Allow uppercase in commit messages

Fixed:
- 🐛(frontend) SVG export
- 🐛(frontend) remove scroll listener table content
- 🔒️(back) restrict access to favorite_list endpoint
- 🐛(backend) refactor to fix filtering on children
    and descendants views
- 🐛(action) fix notify-argocd workflow
- 🚨(helm) fix helmfile lint
- 🚚(frontend) redirect to 401 page when 401 error
2025-03-19 14:11:47 +01:00
Anthony LC
51cc26b916 🐛(frontend) improve svg export to be less pixelized
Some SVGs were pixelized in the exported files.
We now add the wanted size to the svg conversion to
make sure the images are exported with the correct size
and so less pixelized.
2025-03-19 14:11:47 +01:00
Anthony LC
cab8ef51df 🐛(frontend) unmount components Analytics
`useAnalytics` hooks was dispatching methods that
caused children components to be unmounted.
By declaring the methods out of the hook, we can
prevent the components from being unmounted.
2025-03-18 14:53:09 +01:00
Anthony LC
6627518017 🚚(frontend) redirect to 401 page when 401 error
Users could still be able to edit a document if the
session was expired. It could give the feeling that the
document was not saved.
If during a mutation request (POST, PUT, DELETE),
the server returns a 401 error,
the user is redirected to the 401 page.
2025-03-18 14:53:09 +01:00
Pedro Manse
12c18bc4e9 📝(README) Add link to GNU Make
Just like docker-compose, create link to the software's site on it's
first mention.

📝(Changelog) Added entry

📝(Changelog) Added pull request id
2025-03-18 11:07:22 +01:00
Anthony LC
aff330eb5b 🚨(gitlint) Allow uppercase in commit messages
Many developers use uppercase as the first letter
in their commit messages, it creates an error.
We will allow uppercase in commit messages to
lower frustration when committing.
2025-03-18 10:24:08 +01:00
Cameron King
bcdaedba9b 🐛(backend) add user/db to pg healthchecks
Adds PostgreSQL user and database names to the docker-compose.yaml healthchecks.
This resolves an error that appears in the logs, where 'root' is used by
default.
2025-03-18 09:41:27 +01:00
Manuel Raynaud
799814e3e3 🌐(i18n) activate dutch language
All the dutch translations are complete on crowdin. We activate it in
the django settings and download all translations from crowdin
2025-03-18 09:27:13 +01:00
virgile-dev
02c9b2ea2e 🐛(readme) fix preprod link to redirect to homepage (#747)
The current link redirects to a 404. New link redirect to homepage.
2025-03-17 16:02:45 +00:00
Manuel Raynaud
eb23aefd55 ♻️(back) use same base route path for swagger
Swaggers urls where not using the same base route path /api/v1.0, we
prepend it to have the same path everywhere. Moreover, a double slash
was used for swagger and redoc dashboard.
2025-03-17 15:02:34 +01:00
Manuel Raynaud
0c49019490 🚨(helm) fix helmfile lint
Latest release of helmfile is applying the change related before as a
warning. Environnements must be before releases but not in the same
document of repositories.
2025-03-17 14:40:55 +01:00
Anthony LC
170dbe07bb ⬆️(frontend) bump @babel/runtime /src/frontend
Bumps @babel/runtime from 7.26.7 to 7.26.10.
2025-03-17 13:50:20 +01:00
Manuel Raynaud
70136f2415 🐛(action) fix notify-argocd workflow
The notify-argocd workflow was not working correctly. The html_url sent
to argocd was not the good one anymore.
2025-03-17 12:09:18 +01:00
Anthony LC
2a8fc97f2f ⬆️(frontend) bump @babel/helpers in /src/frontend
Bumps @babel/helpers from 7.26.7 to 7.26.10.
2025-03-17 11:50:22 +01:00
Anthony LC
9570701bc3 ⬆️(frontend) bump @babel/runtime /src/mail
Bumps @babel/runtime from 7.26.0 to 7.26.10.
2025-03-17 11:36:04 +01:00
Anthony LC
4b28b3c23b (frontend) add pinning on doc detail
Add pinning button on doc detail page.
2025-03-17 11:16:50 +01:00
Anthony LC
f26fc43df0 🔥(frontend) remove DocTagPublic component
DocTagPublic component was removed because
it was not used.
2025-03-17 11:16:50 +01:00
Anthony LC
05a6818439 🧑‍💻(e2e) display more information when auth fails
When the auth fails, it was quite obscure to
understand what was going on.
We now take a screenshot of the page and display
the console logs.
2025-03-17 09:30:19 +01:00
Anthony LC
8056fd7d66 🚚(frontend) add import path for features/docs tsconfig
To simplify imports, we add the import path @/docs
to target ./src/features/docs/.
We changed all imports to use this path.
2025-03-17 09:30:19 +01:00
Samuel Paccoud - DINUM
c85224af42 📝(README) remove AGPL mention
This mention was confusing. We are only using min.io for development
purposes and this has nothing to do with the project's licence.
2025-03-16 19:00:14 +01:00
Anthony LC
70f1b6a8e8 🚩(frontend) add feature flag on "Copy as HTML"
As a blue print, we add a feature flag on
"Copy as HTML" button in the doc toolbox.
This feature flag is controlled by the `CopyAsHtml`
feature flag.

Be aware:
- if the feature flag is disabled, the button
will be shown
- if the feature flag is enabled and send true,
the button will be shown
- if the feature flag is enabled and send false,
the button will be hidden
2025-03-14 16:26:12 +01:00
Anthony LC
0f07fdcb65 📈(frontend) get user analytics
We will identify users by their email address.
This will help us understand how users interact
with the platform and improve the user experience.
2025-03-14 16:26:12 +01:00
Anthony LC
2e13dfb9bc 📈(frontend) abstract analytics classes
Add abstract classes for analytics services.
We will be able to add easily any analytic
services.
Our first analytic service usecase is Posthog.
2025-03-14 16:26:12 +01:00
renovate[bot]
a026435eb7 ⬆️(dependencies) update canvg to v4.0.3 [SECURITY] 2025-03-14 15:29:27 +01:00
Anthony LC
7007d56c38 🐛(frontend) fix svg not rendering export dox
The svg was not rendering in the dox export.
We overwrite the default mapping to convert the
svg to png before rendering.
The images could be out of the page as well,
we fixed this issue by adding a maxWidth to the image.
2025-03-14 15:13:51 +01:00
Anthony LC
0405e6a3f6 🐛(frontend) fix svg not rendering export pdf
The svg was not rendering in the pdf export.
We overwrite the default mapping to convert the
svg to png before rendering.
The images could be out of the page as well,
we fixed this issue by adding a maxWidth to the image.
2025-03-14 15:13:51 +01:00
Anthony LC
cb8bd4b937 🐛(frontend) fix flakiness e2e tests title doc
- fix multiple states title
- wait for stabilize network after create
- fix test other chromium browsers
- improve grid delete test
2025-03-14 14:49:17 +01:00
Anthony LC
4316b4e67d (frontend) adapt export to divider block
We have a new block type, the divider block.
We have to adapt the export to handle this
new block type.
2025-03-14 10:39:07 +01:00
Anthony LC
534085439f (frontend) add divider blocks to the editor
Add a custom block to add a divider in the editor.
2025-03-14 10:39:07 +01:00
Anthony LC
da02d3d756 🎨(frontend) use blockquote tag for quote block
Use the blockquote tag for quote block instead of
a paragraph tag.
2025-03-13 16:38:33 +01:00
Anthony LC
87960d3773 (AI) add emojify action to ai transform
The emojify action add emojis to the important
parts of the text.
2025-03-13 16:38:33 +01:00
Anthony LC
e0af6d36e1 (AI) add beautify action to ai transform
The beautify action add emojis to the important
parts of the text and add formatting to the text
to make it more readable.
2025-03-13 16:38:33 +01:00
Anthony LC
cbf9091d1c ️(AI) improve formating of ai translation
The ai translation were quite lossy about formatting.
Colors, background, breaklines, table sizes were
lost in the translation.
We improve the AI translation request to keep
the formatting as close as possible by using
html instead of markdown.
2025-03-13 16:38:33 +01:00
Anthony LC
9176328200 ♻️(frontend) replace cors proxy for export
We were using the cors proxy of Blocknote.js
to export the document. Now we use our own proxy
to avoid CORS issues.
2025-03-13 12:39:32 +01:00
Anthony LC
6efc2377fe (back) create a cors proxy fetching docs external resources
When exporting a document in PDF and if the doc contains external
resources, we want to fetch them using a proxy bypassing CORS
restrictions. To ensure this endpoint is not used for something else
than fetching urls contains in the doc, we use access control and check
if the url really exists in the document.
2025-03-13 12:39:32 +01:00
Anthony LC
1c02b0ad8e 📝(frontend) change literal section open source
Change literal section open source.
2025-03-12 09:53:01 +01:00
Manuel Raynaud
007854a877 ️(back) use redis as session backend in developement
We want to persist the session during development. Otherwise the session
is reset everytime the server is restart. This behavior make developing
bot a front and back feature a nigthmare, we spend our time login again
and again
2025-03-12 08:28:20 +01:00
Anthony LC
57cead448d 🏷️(frontend) adapt new doc type nb_accesses_direct
We renamed the `nb_accesses` field to `nb_accesses_direct`
and added a new `nb_accesses_ancestors` field.
We adapt the frontend to use the new fields.
2025-03-11 09:32:48 +01:00
Samuel Paccoud - DINUM
f20d256cd1 🐛(backend) fix numchild when soft deleting/restoring a document
The numchild attribute must be incremented/decremented manually
when we soft delete a document if we want it to remain accurate,
which is important to display the tree structure in the frontend.
2025-03-11 09:32:48 +01:00
Samuel Paccoud - DINUM
76c01df3ae (backend) add number of direct accesses related to a document
The "nb_accesses" field was displaying the number of access instances
related to a document or any of its ancestors. Some features on the
frontend require to know how many of these access instances are related
to the document directly.
2025-03-11 09:32:48 +01:00
Samuel Paccoud - DINUM
20315e9b60 (backend) limit link reach/role select options depending on ancestors
If a document already gets a link reach/role inheriting from one of its
ancestors, we should not propose setting link reach/role on the
document that would be more restrictive than what we inherited from
ancestors.
2025-03-11 09:32:48 +01:00
Samuel Paccoud - DINUM
2203d49a52 (backend) add new "descendants" action to document API endpoint
We want to be able to make a search query inside a hierchical document.
It's elegant to do it as a document detail action so that we benefit
from access control.
2025-03-11 09:32:48 +01:00
Samuel Paccoud - DINUM
56aa69f56a ♻️(backend) refactor list view to allow filtering other views
the "filter_queryset" method is called in the middle of the
"get_object" method. We use the "get_object" in actions like
"children", "tree", etc. which start by calling "get_object"
but return lists of documents.

We would like to apply filters to these views but the it didn't
work because the "get_object" method was also impacted by the
filters...

In a future PR, we should take control of the "get_object" method
and decouple all this. We need a quick solution to allow releasing
the hierchical documents feature in the frontend.
2025-03-11 09:32:48 +01:00
Samuel Paccoud - DINUM
0aabf26694 (backend) add "tree" action on document API endpoint
We want to display the tree structure to which a document belongs
on the left side panel of its detail view. For this, we need an
endpoint to retrieve the list view of the document's ancestors
opened.

By opened, we mean that when display the document, we also need to
display its siblings. When displaying the parent of the current
document, we also need to display the siblings of the parent...
2025-03-11 09:32:48 +01:00
Samuel Paccoud - DINUM
fcf8b38021 (backend) allow forcing page size within limits
We want to be able to increase the page size with by passing
the query string parameter "page_size".
2025-03-11 09:32:48 +01:00
renovate[bot]
757d7f35cd ⬆️(dependencies) update django to v5.1.7 [SECURITY] 2025-03-10 10:19:56 +01:00
Anthony LC
fdc49dc002 🐛(frontend) remove scroll listener table content
During a useEffect cleaning, the selector was
not the correct one. The debounce was not being
removed correctly neither.
2025-03-10 10:06:59 +01:00
Anthony LC
197ba47f73 ⬆️(frontend) bump cunningham to 3.0.0
Last bump to react 19 was a breaking change with
the previous version of Cunnigham, so we need to
update cunningham to 3.0.0 to be compatible with it.
We can now remove Cunnigham from the list of ignored
dependencies in the renovate.json file.
2025-03-10 09:26:19 +01:00
Anthony LC
d5997ba9d5 ⬆️(frontend) bump to react 19.0.0
Last version of Blocknotes is compatible with
React 19.0.0, it seems even necessary to
bump the version of React to 19.0.0.
We bump the version of React to 19.0.0 and
remove the react packages from renovate
list of ignored dependencies.
2025-03-10 09:26:19 +01:00
Anthony LC
1c6d18fdf3 📌(frontend) pin yjs globally
We had a warning about yjs multiple versions
between dependencies. We pinned yjs globally
to avoid this warning and potential side effects.
2025-03-10 09:26:19 +01:00
Anthony LC
24d126f410 🚨(frontend) fix linter
- fix linter
- remove unnecessary style files
2025-03-10 09:26:19 +01:00
renovate[bot]
a5e1751cf3 ⬆️(dependencies) update js dependencies 2025-03-10 09:26:19 +01:00
Manuel Raynaud
0cabb655ad 🔒️(back) restrict access to favorite_list endpoint
favorite_list endpoint is accessible to anonymous user. This lead to an
error 500. This endpoint should be accessible only to authenticated
users.
2025-03-07 09:04:44 +01:00
Manuel Raynaud
38eb6d45b7 🐛(back) fix ValidationError exception handler
The exception handler for the ValidationError was not testing correctly
the existence of some attributes like `message_dict`.
2025-03-07 09:04:44 +01:00
Anthony LC
5bb7ad643a 🔖(minor) release 2.4.0
Added:
- (frontend) synchronize language-choice

Changed:
- Use sentry tags instead of extra scope

Fixed:
- 🐛(frontend) fix collaboration error
2025-03-06 15:59:34 +01:00
Anthony LC
57b8881fc6 📌(frontend) pin blocknote globally
Blocknote does not pinned the version.
We get bumped version instead of the version we want.
We pin the version of blocknote globally to
avoid this issue.
2025-03-06 15:09:17 +01:00
Anthony LC
89ad610ba6 🐛(frontend) fix collaboration error
We upgrade blocknote to 0.23.2-hotfix.0,
it includes a fix with the collaboration.
2025-03-06 15:09:17 +01:00
rvveber
251787b835 🚨(tests) add language related tests; fix getByRole not working in tests
- adds tests and test-utility for solid language switching in tests
- fixes where ...getByRole(menuitem... would not return a valid object
2025-03-05 14:29:24 +01:00
rvveber
f95173e096 🐛(frontend) allow left panel to update on language change
- fixes a bug where after language-sync the left panel would remain
  in the same language as before.
2025-03-05 14:29:24 +01:00
rvveber
a7944cce80 (app) get language from backend; set browser-detected language if null
- adds useLanguageSynchronizer hook to update the:
  1. frontend-language to the user-preference - if there is one.
  2. user-preference to the (browser-detected) frontend-language - otherwise.
2025-03-05 14:29:24 +01:00
rvveber
7941fc91d5 🐛(frontend) set toolbar-popup to current language
- ensure editor is translated to i18n.resolvedLanguage => en
  as i18n.language could hold more accurate locale => en-GB etc..
2025-03-05 14:29:24 +01:00
rvveber
7fc83a4fcd (frontend) the LanguagePicker now uses config as options
- config endpoint languages are used as available options for LanguagePicker
- updating the language from it, triggers an update on the user via API
2025-03-05 14:29:24 +01:00
rvveber
2bf47b7705 (backend) the LanguagePicker now uses config as options
- config endpoint languages are used as available options for LanguagePicker
- updating the language from it, triggers an update on the user via API
2025-03-05 14:29:24 +01:00
rvveber
23b0214a2a (frontend) add language utility for "locale"
- Adds a helper for working with locales
- More details in their annotations
- Unnecessary, if in the future, the backend uses
  the same locales as the keys in the translations (ISO 639-1)
2025-03-05 14:29:24 +01:00
rvveber
f244509de3 (frontend) add API access for 'language' attribute on User model
- allow the language attribute on the user to be updated via API
- add frontend function to update the user language via API
- extend defaults on the test users, to have fixed language in E2E tests
- extend types and variables using the types with the new field
2025-03-05 14:29:24 +01:00
rvveber
fda5f8f008 (backend) add API access for 'language' attribute on User model
- allow the language attribute on the user to be updated via API
- add frontend function to update the user language via API
- extend defaults on the test users, to have fixed language in E2E tests
- extend types and variables using the types with the new field
2025-03-05 14:29:24 +01:00
rvveber
9a79b09b07 🔨(backend) make the 'language' attribute on the User model nullable
- allow the language on the user to be unset
- set the default language to be unset
- helps us determine that the user has yet to set a language preference
2025-03-05 14:29:24 +01:00
rvveber
b24acd14e2 🔨(frontend) email invitation in invited user's language
- language for invitation emails => language saved on the invited user
- if invited user does not exist yet => language of the sending user
- if for some reason no sending user => system default language
2025-03-05 14:29:24 +01:00
rvveber
1531846115 🔨(backend) email invitation in invited user's language
- language for invitation emails => language saved on the invited user
- if invited user does not exist yet => language of the sending user
- if for some reason no sending user => system default language
2025-03-05 14:29:24 +01:00
Manuel Raynaud
ebf6d46e37 ♻️(front) use sentry tags instead of extra scope
To ease filtering issues on sentry, we want to use tags instead of extra
scope. Tags are indexed and searchable, it's not the case with extra
scope. Moreover using setEtra to add additional data is deprecated.
2025-03-05 10:26:23 +01:00
Manuel Raynaud
b9b5f86cf4 ️(back) restrict documents to restore using only the queryset
To determine the descendant to restore or not, we were looking building
a complex exclude clause. This can be simplify focusing only on data we
already have without making an extra query to fetch the list of
descendant to exclude.
2025-03-04 18:03:18 +01:00
renovate[bot]
56412b0be5 ⬆️(dependencies) update python dependencies 2025-03-04 14:24:58 +01:00
Anthony LC
af052cd06b 🔖(minor) release 2.3.0
Added:
- 💄(frontend) add error pages
- 🔒️ Manage unsafe attachments
- (frontend) Custom block quote with export
- (frontend) add open source section homepage

Changed:
- 🛂(frontend) Restore version visibility
- 📝(doc) minor README.md formatting and wording enhancements
- ♻️Stop setting a default title on doc creation
- ♻️(frontend) misc ui improvements

Fixed:
- 🐛(backend) allow any type of extensions for media download
- ♻️(frontend) improve table pdf rendering
2025-03-04 12:12:57 +01:00
Anthony LC
8927635c5f 💄(frontend) hide Crisp when mobile and modal
The Crisp button was hidding buttons on mobile
when a modal was open. This commit hides the
Crisp button when a modal is open on mobile.
2025-03-04 12:12:57 +01:00
Anthony LC
76bce4313b 🩹(frontend) fine tuning 2.3.0
- improve medium button style when 2 lines
- improve design on Firefox input title
- manage title modal without doc title
- improve redirect when 401
2025-03-04 12:12:57 +01:00
Anthony LC
5ac71bfac1 🐛(service-worker) update sw to create a doc without body
Offline creation of a doc was broken because we
don't add a default title anymore when we create a
doc, leading to POST requests without body.
we need to adapt the service worker to handle this
case.
2025-03-04 12:12:57 +01:00
Anthony LC
cb4e148afc ♻️(email) adapt email when no title
Default title is not set when we create a document
anymore. We need to adapt the email to handle
this case.
2025-03-04 12:12:57 +01:00
AntoLC
2d24825be0 🌐(i18n) update translated strings
Update translated files with new translations
2025-03-04 12:12:57 +01:00
Anthony LC
7b1ddc0e05 🛂(backend) remove svg from unsafe
We added content-security-policy on nginx.
It should be safe to allow svg files now.
We remove the svg file from the unsafe
attachments list. We adapt the tests accordingly.
2025-03-03 13:18:40 +01:00
Manuel Raynaud
22a665e535 🔒️(nginx) manage Content-Security-Policy in nginx config
The media route is managed by nginx. On this route we want to add the
Content-Security-Header to forbid fetching any resources.
See : https://content-security-policy.com/
2025-03-03 13:18:40 +01:00
Manuel Raynaud
a22bf95bce 🔒️(back) set ContentDisposition on media upload
On the media upload endpoint, we want to set the content-disposition
header. Its value is based on the uploaded file mime-type and if flagged
as unsafe. If the file is not an image or is unsafe then the
contentDisposition is set to attachment to force its download.
Otherwise, we set it to inline.
2025-03-03 13:18:40 +01:00
Anthony LC
3ce1826355 🚚(frontend) toolbar components in BlockNoteToolBar folder
We moved the toolbar components in BlockNoteToolBar
folder.
2025-03-03 13:18:40 +01:00
Anthony LC
d099d58f77 🛂(frontend) secure download button
Blocknote download button opens the file in a new
tab, which could be not secure because of XSS attacks.
We replace the download button with a new one that
downloads the file instead of opening it in a new tab.
Some files are flags as unsafe (SVG / js / exe),
for these files we add a confirmation modal before
downloading the file to prevent the user from
downloading a file that could be harmful.
In the future, we could add other security layers
from this model, to analyze the file before
downloading it by example.
2025-03-03 13:18:40 +01:00
Anthony LC
ebd49f05a8 🚸(frontend) block click on unsafe image
We want to prevent the user to open unsafe images
in the browser. We blocked the click on the images.
To download them, the user will have to use the
download button.
2025-03-03 13:18:40 +01:00
Anthony LC
315c2c2c43 🐛(frontend) improve authenticated state
It can happen that the user is authenticated
then the token is expired. The authenticated
state should be updated to false in this case.
2025-03-03 13:18:40 +01:00
Anthony LC
e442908c50 💄(frontend) improve the design of the alert error
Since the new design implementation,
the alert error was not looking good.
This commit improves the design of the alert error.
2025-03-03 13:18:40 +01:00
Anthony LC
6672292d93 🛂(backend) add unsafe in the attachments filename
The frontend cannot access custom headers of a file,
so we need to add a flag in the filename.
We add the `unsafe` flag in the filename to
indicate that the file is unsafe.

Previous filename: "/{UUID4}.{extension}"
New filename: "/{UUID4}-unsafe.{extension}"
2025-03-03 13:18:40 +01:00
Manuel Raynaud
7dda74421f ♻️(back) extract ancestor deleted_at directly from db in restore method
In the restore method, all the ancestors with a deleted_at date set are
extracted from the database and then the oldest value is extracted using
the min python function. This usage of min can be removed by sorting
directly the deleted_at at the databse level and then fetching the first
one. It's faster and easier to maintain.
2025-03-03 13:05:36 +01:00
Anthony LC
9c25b684e3 (frontend) add open source section homepage
We decided to add the open source section on
the homepage of Docs.
2025-03-03 12:42:18 +01:00
Anthony LC
cd5ee3fb7c (frontend) adapt export to quote block
We have a new block type, the quote block.
We have to adapt the export to handle this
new block type.
2025-03-03 12:27:02 +01:00
Anthony LC
942c0f059c 🏗️(frontend) blockMapping refactoring
As made for TablePDF, we separate the block mapping
in separate files. This will allow us to have
a better separation of concerns and to have
a more maintainable codebase.
We improve as well the typing. It will be easier
to add new blocks in the future.
2025-03-03 12:27:02 +01:00
Anthony LC
3acee1e6fa (frontend) create feature doc-export
Create the feature doc-export, it will be
responsible for exporting the document.
2025-03-03 12:27:02 +01:00
Anthony LC
26ea32bd0b 🏷️(frontend) adapt title types
We recently changed the default title behavior.
It can now be undefined, we have to change the
types accordingly.
2025-03-03 12:27:02 +01:00
Anthony LC
7f6ffa0123 (frontend) add quote blocks to the editor
Add a custom block to quote in the editor.
2025-03-03 12:27:02 +01:00
Samuel Paccoud - DINUM
ef2127585c 🐛(backend) allow any type of extensions for media download
The regex to validate media file extensions was too restrictive.
2025-03-03 11:21:41 +01:00
Anthony LC
54a75bc338 ♻️(frontend) update some elements
- add panel information when document is
authenticated
- add a copy link button in the toolbox
on the document
- fix when long title document
- modals fit design
- mobile responsive changes
2025-02-24 10:49:20 +01:00
Anthony LC
50d098c777 💄(frontend) adapt language picker design
Adapt the language picker design to
to fit with the new design of the app.
2025-02-24 10:49:20 +01:00
Anthony LC
757c09b189 ♻️(frontend) adapt other error pages to new design
Adapt the other error pages to the new design.
2025-02-24 10:27:42 +01:00
Anthony LC
30c5cfab62 💄(frontend) add page 401
Add a 401 page when user try to access a
doc that need authentication.
2025-02-24 10:27:42 +01:00
Anthony LC
f069329e18 💄(frontend) add page 403
Add a page and integrate the design for the
403 error page.
2025-02-24 10:27:42 +01:00
Manuel Raynaud
ef8ee67553 ♻️(compose) set compose name in the docker-compose file
The helper bin/compose was using the option -p to set the compose
project name but this option is not used in the Makefile. This can lead
to different way to use the docker compose file definition with
different project name. In order to have a consistent name everywhere
and for everybody, we set the name in the docker compose file itself.
2025-02-20 10:32:37 +01:00
Anthony LC
ad47fc2d60 (e2e) global setup authentication
Because of the parallelism of the tests,
the authentication setup was flaky. Sometimes
the tests would run before the authentication
was complete.
We change to a global setup instead of the
project dependency setup, it should be more
reliable.
We improved the waiting states of the authentication
setup.
2025-02-19 14:57:15 +01:00
Nathan Vasse
009f5d6ed4 ♻️(front) improve table pdf rendering
The previous way of rendering table was causing issues when tables
could not fit on one page. I then came accross this discussion
https://github.com/diegomura/react-pdf/issues/2343. The author
created a lib to improve the rendering of table, it's better, but
still not perfect maybe.
2025-02-19 13:58:29 +01:00
Nathan Vasse
64d0072c8d 🐛(front) improve text rendering in pdf
The rendered text had unwanted line breaks in middle of them.
It was because we were not using the appropriate Text component, the
one to be used in the one from react-pdf.
2025-02-18 17:01:58 +01:00
Anthony LC
aefbc2e0b9 🛂(frontend) hide version restore button when reader
When you are a reader member, you have the right to
see the version but you cannot restore
them. So, we hide the restore button
when you are a reader.
2025-02-18 09:30:37 +01:00
Anthony LC
15dc1e3012 🦺(migration) add back the migration folders to linter
Previous commit add "core/tests/migrations".
The linter could not pass on it because all the
migration folders were excluded from the linter.
We remove this exclusion, tests and migrations can
now be linted and formatted automatically.
2025-02-15 23:39:17 +01:00
Anthony LC
6cc20aeacb 🩹(migration) add migration to update default titles
The frontend was setting a default titles for
documents with empty titles.
This migration updates the document table to set
the title to null instead of the default title.
We add a test to ensure that the migration
works as expected.
2025-02-15 23:39:17 +01:00
Anthony LC
7da7214afb (backend) add django-test-migrations
Add django-test-migrations to the project.
It is a tool that helps to test Django migrations.
2025-02-15 23:39:17 +01:00
Anthony LC
c369419512 ♻️(frontend) stop setting a default title on doc creation
We were setting a default title to our document
during creation, but we should not do that,
it created lot of similar titles, lot of
documents will show up during search.
2025-02-15 23:39:17 +01:00
Anthony LC
d9ad397c94 🩹(frontend) minor fixes
- fix linter warning on one e2e test
- improve logo svg
- improve cursor
- improve grid loader
2025-02-15 23:39:17 +01:00
Manuel Raynaud
3191d890f3 ♻️(docker) rename frontend-dev service in frontend
The frontend-dev service is in fact using the production image. We
rename it in frontend accordingly with what it really does. We also have
to change name rules in Makefile to be consistent.
2025-02-14 19:54:38 +01:00
Manuel Raynaud
68f3387539 ♻️(make) make run command starting everything
The run command is not starting the frontend application. We change the
run commands. The run command is strating everything. The run-backend
command is starting all services needed to use the backend application.
2025-02-14 19:54:38 +01:00
Manuel Raynaud
0dc8b4556c ♻️(docker) remove usage of dockerize
We remove dockerize and use healthcheck on docker compose services
instead.
2025-02-14 19:54:38 +01:00
Manuel Raynaud
e123e91959 🐛(nginx) increase nginx buffer size when proxifying keycloak
Nginx is used to proxify keycloak in our development configuration. When
a new user is created keycloak is send a large amount of headers in its
response and the default nginx config is not enough to handle this
amount of headers. We have to increase the proxy buffer size to handle
them.
2025-02-14 19:54:38 +01:00
Bastien Guerry
2709400773 🔊(changelog) add a changelog entry
Add "📝(doc) minor README.md formatting and wording enhancements" in
the unreleased section.
2025-02-14 17:39:52 +01:00
Bastien Guerry
8281c6159b 📝(doc) minor README.md formatting and wording enhancements
See https://github.com/suitenumerique/docs/issues/622
2025-02-14 17:39:52 +01:00
Anthony LC
296dbb7957 🔖(minor) release 2.2.0
Added:
- 📝(doc) Add security.md and codeofconduct.md
- (frontend) add home page
- (frontend) cursor display on activity
- (frontend) Add export page break

Changed:
- 🔧(backend) make AI feature reach configurable

Fixed:
- 🌐(CI) Fix email partially translated
- 🐛(frontend) fix cursor breakline
- 🐛(frontend) fix style pdf export
2025-02-11 14:16:58 +01:00
Anthony LC
3827f0f799 🩹(frontend) fine tuning v2.2.0
Fix minor issues:
- Add some height to the carret in the editor
- Improve css in mail templates
- Improve images resolution on homepage
2025-02-11 14:16:58 +01:00
Anthony LC
d89e3dc6d4 🛂(frontend) display the AI buttons depend abilities
Anybody with edit right could use the AI.
We changed this behavior, now we have to be
authentified with edit right.
We update the UI to display the AI buttons
only if the user has the correct AI ability.
2025-02-11 10:54:55 +01:00
Samuel Paccoud - DINUM
91cf5f9367 🔧(backend) make AI feature reach configurable
We want to be able to define whether AI features are available to
anonymous users who gained editor access on a document, or if we
demand that they be authenticated or even if we demand that they
gained their editor access via a specific document access.

Being authenticated is now the default value. This will change the
default behavior on your existing instance (see UPGRADE.md)
2025-02-11 10:54:55 +01:00
Anthony LC
5cc4b07cf6 💄(frontend) improve caret style
Improve the caret, to looks more like the
google doc caret.
2025-02-10 15:53:03 +01:00
Anthony LC
0cfc242e09 🚚(frontend) move Blocknote styles
Move Blocknote styles to a separate file.
2025-02-10 15:53:03 +01:00
Anthony LC
a6b3cfdb0c (frontend) add export page break
Blocknotejs introduced the ability to export a
document with page breaks.
This commit adds the page break feature to the
editor and so to our export feature.
2025-02-10 15:53:03 +01:00
Anthony LC
5ead18c94c 💬(frontend) add lacking buttons open source section
Some buttons were lacking in the open source
section of the home page. This commit adds them.
2025-02-10 15:40:23 +01:00
AntoLC
5eeb8cae5c 🌐(i18n) update translated strings
Update translated files with new translations
2025-02-10 15:40:23 +01:00
Jacques ROUSSEL
68bf024005 (helm) add pdbs to deployments
In order to avoid a service interruption during a Kubernetes (k8s)
upgrade, we add a Pod Disruption Budget (PDB) to deployments.
2025-02-10 13:05:54 +01:00
Anthony LC
fdd1068c90 (frontend) fix test copy html
The recent upgrade of Blocknote has caused the
test to fail.
This commit updates the test to reflect
the new html structure.
2025-02-10 10:50:29 +01:00
Anthony LC
ba695bf647 ⬇️(frontend) downgrade to @react-pdf/renderer to 4.1.6
The version 4.2.1 of @react-pdf/renderer
is not compatible @blocknote/xl-pdf-exporter
and @blocknote/xl-docx-exporter.
2025-02-10 10:50:29 +01:00
Anthony LC
27e7aec193 💄(frontend) improve styles export pdf
When exporting a document to PDF, the headings
spacings were too small, the break lines were
not displayed. This commit fixes these issues
by replacing the needed blocks.
2025-02-10 10:50:29 +01:00
Anthony LC
58b712a1de 🐛(frontend) fix cursor breakline
We had breakline issues with the initial
cursor because of some css properties.
We changed the cursor css to not take
any space in the lines,
avoiding the breakline issues.
We keep the new cursor visibility
feature (always, activity).
2025-02-10 10:50:29 +01:00
renovate[bot]
08f9036523 ⬆️(dependencies) update js dependencies 2025-02-10 10:50:29 +01:00
Anthony LC
ebe3efc8f7 💄(frontend) update the favicon
Update the favicon with a better one.
2025-02-07 18:18:22 +01:00
Anthony LC
66fbf27913 🩹(frontend) fix PageLayout
The page layout was rendered behind the header,
which caused the top of the mention legales pages
to be hidden.
This commit fixes this issue.
2025-02-07 18:18:22 +01:00
Anthony LC
20e4a4e42a 🥚(frontend) easter egg in dev tools
Add a easter egg in the browser dev tools.
2025-02-07 18:18:22 +01:00
Anthony LC
1aa4844eeb 🎨(frontend) add dsfr proconnect homepage
If we are with the DSFR theme, we need to add the
proconnect button to the homepage.
We add an option in the cunningham theme to
display the proconnect section instead of the
opensource section.
2025-02-07 18:18:22 +01:00
Nathan Panchout
4bb9c092cb (frontend) add white label homepage
Add white label homepage with new assets and
components. When the user is not logged in,
the homepage will be displayed.
2025-02-07 18:18:22 +01:00
Anthony LC
c493eb8924 ♻️(frontend) use a hook instead of a store for auth
We will use a hook instead of a store for the auth
feature. The hook will be powered by ReactQuery,
it will provide us fine-grained control over the
auth state and will be easier to use.
2025-02-07 18:18:22 +01:00
Anthony LC
40fdf97520 🚚(frontend) move auth to its own feature
We will move auth to its own feature to make it
easier to manage and to make it more modular.
2025-02-07 18:18:22 +01:00
Anthony LC
91b10e75dd 💄(email) fix line height email title
The line height of the email title was not
the correct size. We let the title managing
its own line height.
2025-02-07 17:05:49 +01:00
Anthony LC
7a6da10e1c 🐛(i18n) add back the missing email translations
We changed the way we upload the translations to
Crowdin, some translations were missing for the
email templates. We add them back and improve
the tests to make sure we don't forget them again.
2025-02-07 17:05:49 +01:00
Anthony LC
004e8ec645 🌐(CI) build mails when crowdin_upload workflow
When we were executing the crowdin_upload workflow,
we were not building the mail template to dispatch it
to the backend. It resulted in the mail not being
totally translated. This commit fixes that issue
by adding the build mail step to the crowdin_upload.
To do so, we added it to the dependencies workflow.
"dependencies" workflow is callable by other
workflows that need a specific job.
2025-02-07 17:05:49 +01:00
virgile-deville
a1bca9c436 📝(doc) add security.md and conduct.md policies
We need a safe way for people to report vulnerabilities.
People now can go on SECURITY.md and follow our policy.

We want to have a policy for expected behaviour.
People can check out CODE_OF_CONDUCT.md.
2025-02-02 14:53:29 +01:00
Sylvain Zimmer
d02fa1ddd4 ✏️(readme) fix a few typos
Found a few errors after proof-reading the README
2025-01-30 19:00:56 +01:00
Anthony LC
1fd66d3081 🔖(minor) release 2.1.0
Added:
- (backend) add soft delete and restore API endpoints to documents
- (backend) allow organizing documents in a tree structure
- (backend) add "excerpt" field to document list serializer
- (backend) add github actions to manage Crowdin workflow
- 📈Integrate Posthog
- 🏷️(backend) add content-type to uploaded file
- (frontend) export pdf docx front side7

Changed:
- 💄(frontend) add abilities on doc row
- 💄(frontend) improve DocsGridItem responsive padding
- 🔧(backend) Bump maximum page size to 200
- 📝(doc) Improve Read me

Fixed:
- 🐛Fix invitations

Removed:
- 🔥(backend) remove "content" field from list serializer
2025-01-30 12:50:44 +01:00
AntoLC
bae8c4c563 🌐(i18n) update translated strings
Update translated files with new translations
2025-01-30 11:37:05 +01:00
Anthony LC
0dae35dab1 ✏️(frontend) replace Word / Open Office by Docx
Replace the naming of the select export
options from Word / Open Office to Docx.
2025-01-30 11:37:05 +01:00
Anthony LC
929a50b573 ⬇️(frontend) downgrade cunningham to 2.9.4
The last version of Cunningham has problems.
Better to downgrade to the previous version.
We add cunningham library to renovate.json
to prevent future upgrade with Renovate.
An issue will be open to upgrade to the
last version manually.
2025-01-29 17:45:14 +01:00
Anthony LC
83dfd26d1c ⬇️(frontend) downgrade to react 18
We still have conflict with React 19, better to
downgrade to react 18 for the moment.
We add the react 18 libs to renovate.json
to prevent future upgrade with Renovate.
An issue will be open to upgrade to React 19
manually.
2025-01-29 17:45:14 +01:00
renovate[bot]
addc6a331f ⬆️(dependencies) update js dependencies 2025-01-29 17:45:14 +01:00
Samuel Paccoud - DINUM
5c5763a0ef 🗃️(backend) make document/template creation atomic
When creating a new document/template via the API, we add the
logged-in user as owner of the created object. This should be
done atomically with the object creation to make sure we don't
end-up with an orphan object that the creator can't access
anymore.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
5042f4ca47 ♻️(backend) generalize use of SerializerPerActionMixin in viewsets
We improve the serializer and generalize its use for clarity.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
8c247c8777 🔧(backend) bump maximum page size to 200
The new UI with infinite scroll calls for longer pages which we
are able to produce thanks to the many optimizations on the list
view.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
239342fbbd (backend) add API endpoint action to restore a soft deleted document
Only owners can see and restore deleted documents. They can only do
it during the grace period before the document is considered hard
deleted and hidden from everybody on the API.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
8ccfdb3c6a (backend) add soft delete to documents and refactor db queryset
Now that we have introduced a document tree structure, it is not
possible to allow deleting documents anymore as it impacts the whole
subtree below the deleted document and the consequences are too big.

We introduce soft delete in order to give a second thought to the
document's owner (who is the only one to be allowed to delete a
document). After a document is soft deleted, the owner can still
see it in the trashbin (/api/v1.0/documents/trashbin).
After a grace period (30 days be default) the document disappears
from the trashbin and can't be restored anymore. Note that even
then it is still kept in database. Cleaning the database to erase
deleted documents after the grace period can be done as a maintenance
script.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
4de03d292a (backend) add API endpoint to move a document in the document tree
Only administrators or owners of a document can move it to a target
document for which they are also administrator or owner.

We allow different moving modes:
- first-child: move the document as the first child of the target
- last-child: move the document as the last child of the target
- first-sibling: move the document as the first sibling of the target
- last-sibling: move the document as the last sibling of the target
- left: move the document as sibling ordered just before the target
- right: move the document as sibling ordered just after the target

The whole subtree below the document that is being moved, moves as
well and remains below the document after it is moved.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
2e8a399668 (backend) override defaults.NB_OBJECTS for the demo tests
Otherwise when you increase the number of objects to test how the
application scales, the demo test will take too long...
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
7b39b3f7f6 (backend) improve test error
This test was missing the status code check. Without this check
the error that follows does not make sense because the content
returned is not at all what we expect in the following assert
statement.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
0003f9d0de (backend) add user roles as field in the document API representation
user roles were already computed as an annotation on the query for
performance as we must look at all the document's ancestors to determine
the roles that apply recursively. We can easily expose them as readonly
via the serializer.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
8117866ce7 ♻️(backend) remove content from list serializer and introduce excerpt
Including the content field in the list view is not efficient as we need
to query the object storage to retrieve it. We want to display an excerpt
of the content on the list view so we should store it in database. We
let the frontend compute it and save it for us in the new "excerpt" field
because we are not supposed to have access to the content (E2EE feature coming)
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
1d0386d9b5 (backend) add API endpoint to create children for a document
We add a POST method to the existing children endpoint.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
7ff4bc457f (backend) add API endpoint to list a document's children
This endpoint is nested under a document's detail endpoint.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
4333b46901 (backend) add depth, path and numchild to serialized document
This information is useful for the frontend to display the document
tree structure and is cheap to expose.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
d073a9c9b3 (backend) retrieve & update a document taking into account ancestors
A document should inherit the access rights a user has on any of its
ancestors.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
48662ceecb (backend) list only the first visible parent document for a user
Now that we have a tree structure, we should only include parents of
a visible subtree in list results.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
9a12452c26 🚚(backend) split test files to make place for tests on tree structure
The test_api_documents_list file was getting too long. We can extract
tests on filters and ordering.
2025-01-29 14:39:47 +01:00
Samuel Paccoud - DINUM
276b4f7c1b (backend) add django-treebeard to allow tree structure on documents
We choose to use Django-treebeard for its quality, performance and
stability. Adding tree structure to documents is as simple as
inheriting from the MP_Node class.
2025-01-29 14:39:47 +01:00
Anthony LC
0189078917 (CI) fix test-e2e-other-browser job
A recent update to the impress-frontend workflow
caused the test-e2e-other-browser job to fail.
This commit fixes the issue.
2025-01-29 14:23:46 +01:00
Anthony LC
6569f61fc4 (e2e) fix flakiness
Some flakiness appeared in the e2e tests. It
started to impact many pull requests. Time to
fix them.
2025-01-29 14:23:46 +01:00
Nathan Panchout
7880391648 ️(frontend) rollback test changes
Changes were pushed by mistake. We go back. We reorder the tests
correctly and return the environment variables to their original state.
2025-01-29 12:16:10 +01:00
Nathan Panchout
9b95a9c551 🚸(frontend) prevent duplicate invite user row in DocShareModal
Modify the DocShareModal to avoid showing the invite user row when the
email already exists in the search results, preventing redundant invite
options
2025-01-29 12:16:10 +01:00
Manuel Raynaud
3b151cf580 🍱(readme) update europe <3 opensource logo name
The name used for the logo europe<3opensource was causing trouble on
windows systems. We have to rename it with a more "normal" name.
2025-01-29 12:16:10 +01:00
virgile-deville
8b0f4db650 📝(doc) improve readme.md after v2 update
We want to serve as an example of our open source doc best practices.
We want people to find out.
- Which libraries we support
- How they can contribute translations and code
2025-01-28 20:26:40 +01:00
Nathan Panchout
a39990d90f 🚸(frontend) simplify invite user row logic in DocShareModal
Refactor the endActions logic to show invite user row when searching by
email, removing the unnecessary length check for users
2025-01-28 18:15:18 +01:00
Samuel Paccoud - DINUM
609ff91894 🚸(backend) on user search match emails by Levenstein distance
When the query looks like an email (includes @) we search by
Levenstein distance because we are just trying to prevent typing
errors, not searching anymore.

It is important to still propose results with a short Levenstein
distance because it is frequent to forget a double letter in
someone's name for example "Pacoud" or even "pacou" instead of
"Paccoud" and we want to prevent duplicates or failing on
invitation.

We consider the query string to be an email as soon as it contains
a "@" character. Trying harder to identify a string that is really
an email would lead to weird behaviors like toto@example.gouv looking
like and email but if we continue typing toto@example.gouv.f not
looking like an email... before toto@example.gouv.fr finally looking
like an email. The result would be jumping from one type of search
to the other. As soon as there is a "@" in the query, we can be
sure that the user is not looking for a name anymore and we can
switch to matching by Levenstein distance.
2025-01-28 18:15:18 +01:00
Manuel Raynaud
265a24fe7e 🚨(back) update code linting with ruff
Apply new rules provided with ruff version v0.9.3
2025-01-28 16:23:07 +01:00
renovate[bot]
9bc2b4877f ⬆️(dependencies) update python dependencies 2025-01-28 16:23:07 +01:00
Nathan Panchout
b93b43abe8 💄(frontend) improve DocsGridItem responsive padding
- Adjusted padding and alignment for desktop and mobile views
- Conditionally applied CSS styles based on screen size
2025-01-28 13:59:22 +01:00
Anthony LC
dd8bb18f69 🔊(changelog) add some changelog entries
Add some changelog entries that can be useful to
display in the release notes.
2025-01-28 13:36:03 +01:00
Anthony LC
545e8b2a3c 🔥(backend) remove code related to export pdf docx
The export is managed by the frontend, so we
don't need the code related to the export
in the backend side anymore.
2025-01-28 13:36:03 +01:00
Anthony LC
81837aff2b (frontend) export pdf docx front side
We have added the export to pdf and docx feature
to the front side. Thanks to that, the images are now
correctly exported even when the doc is private.
To be able to export the doc, the data must be
in blocknote format, for legacy purpose, we have
to convert the template to blocknote format before
exporting it.
2025-01-28 13:36:03 +01:00
lunika
40c1107959 🌐(i18n) update translated strings
Update translated files with new translations
2025-01-28 12:59:54 +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
Anthony LC
a090f180f4 💄(frontend) make favicon more visible
In dark mode, the favicon is not correctly visible.
This commit makes it more visible by adding some
white color to the favicon.
2025-01-13 18:15:25 +01:00
Anthony LC
c7e543d459 🐛(frontend) switch to other provider
When we redirect from a doc to another, the components
are not unmounted and states are not reset.
We now destroy the provider if we see that
the provider is not bind to the current doc.
2025-01-13 18:15:25 +01:00
Anthony LC
23e6b508f8 🚚(frontend) harmonize imports
Harmonize the imports in the frontend codebase
to keep the codebase consistent.
2025-01-13 11:02:24 +01:00
Nathan Panchout
49a3989977 💄(frontend) fix minor bugs and enhance DocTitle and DocShareModal
- Fixed minor bugs in the frontend codebase for improved stability.
- Enhanced DocTitle component to update title display dynamically using
useEffect.
- Refactored DocShareModal to improve modal content height calculation
for better responsiveness.
2025-01-13 11:02:24 +01:00
Nathan Panchout
8eb2b60937 (frontend) enhance document grid tests and component interactions
- Updated test cases to replace 'docs-grid-loader' with 'grid-loader'
for improved consistency across document grid tests.
- Refactored document interaction tests to utilize the 'docs-grid'
locator for better readability and maintainability.
- Enhanced document table content tests by refining element selection
methods for improved clarity and performance.
- Cleaned up test code to ensure better structure and maintainability.
2025-01-13 11:02:24 +01:00
Nathan Panchout
f02dcae52a (frontend) enhance DocShareModal and footer components
- Refactored DocShareModal to improve user experience with dynamic list
height and responsive design adjustments.
- Introduced new styling for modal elements using createGlobalStyle for
better visual consistency.
- Updated footer button text from 'Ok' to 'OK' for improved clarity.
- Enhanced user selection handling and search functionality within the
modal for better document sharing experience.
2025-01-13 11:02:24 +01:00
Nathan Panchout
098df5c0b5 (frontend) enhance UI components and improve document management
- Updated DropdownMenu to include index-based styling for better visual
consistency.
- Refactored QuickSearchStyle to remove unnecessary transitions for
smoother performance.
- Adjusted modal styles in cunningham-style.css for improved layout.
- Changed BlockNoteEditor to update block type from 'heading' to
'paragraph' for better content structure.
- Enhanced DocHeader and DocToolBox components with updated color themes
for improved visibility.
- Modified ModalRemoveDoc to change size and clean up unnecessary props
for better usability.
- Improved Heading and TableContent components to handle empty states
more gracefully.
- Updated DocsGrid to conditionally render content based on document
availability, enhancing user experience.
- Refined LeftPanel components for better layout and visual hierarchy,
including adjustments to padding and separators.
2025-01-13 11:02:24 +01:00
Nathan Panchout
684b77cbe6 (frontend) enhance DocsGrid component and simplify layout
- add scroll inside the doc grid
2025-01-13 11:02:24 +01:00
Nathan Panchout
81e9fc49fe (frontend) enhance document interaction tests
- Updated test cases to improve accessibility by replacing 'more_vert'
with 'more_horiz' for action buttons across various components.
- Refactored document deletion confirmation messages to use consistent
heading roles for better visibility.
- Simplified keyboard interactions in document table content tests for
improved clarity.
- Adjusted visibility checks for the share button to utilize more
descriptive labels.
- Cleaned up test code for maintainability and consistency.
2025-01-13 11:02:24 +01:00
Nathan Panchout
7c696fc1ec (frontend) enhance document sharing components
- Refactored DocShareAddMemberList to simplify button styling and
improve loading state handling.
- Updated DocShareAddMemberListItem and DocShareMemberItem to enhance
spacing and button color for better visual consistency.
- Improved DocShareInvitationItem and SearchUserRow with new theming and
spacing tokens for a more cohesive design.
- Adjusted padding and layout in DocShareModal and DocShareModalFooter
for improved responsiveness.
- Enhanced DocVisibility component with updated padding and text styling
for better readability.
- Cleaned up unused imports and optimized component structures for
maintainability.
2025-01-13 11:02:24 +01:00
Nathan Panchout
fc27043e9e (frontend) enhance document editor and header components
- Improved styling for headings in BlockNoteEditor for better visual
hierarchy.
- Adjusted padding in DocEditor and DocHeader based on device type for
responsive design.
- Updated DocTitle and ModalExport components to enhance typography and
spacing.
- Refactored DocToolBox to improve share button functionality and access
display.
- Enhanced versioning modal with better layout and accessibility
features.
- Cleaned up unused imports and optimized component structures for
maintainability.
2025-01-13 11:02:24 +01:00
Nathan Panchout
6ad1e27acf (frontend) enhance UI components and improve styling
- Updated DropdownMenu and ButtonLogin components for better
accessibility and visual consistency.
- Refactored Header and Title components to utilize new theming and
spacing tokens.
- Enhanced LanguagePicker styles for improved user experience.
- Introduced new utility functions in doc-management for better handling
of ProseMirror nodes and Yjs integration.
- Cleaned up unused imports and adjusted component styles for overall
code maintainability.
2025-01-13 11:02:24 +01:00
Nathan Panchout
899047d9a2 (frontend) enhance QuickSearch components
- Updated QuickSearchItemContent to ensure full width for better layout
consistency.
- Adjusted padding in QuickSearchStyle for improved spacing and visual
hierarchy.
- Refactored DocSearchItem to utilize Box component for consistent
styling and layout.
- Removed unused imports in DocSearchItem to streamline the codebase.
2025-01-13 11:02:24 +01:00
Nathan Panchout
78b5e2c1cc (frontend) enhance document grid
- Updated the layout and styling of the DocsGrid and DocsGridItem
components for improved responsiveness and visual consistency.
- Added a new background prop to the UserAvatar component for
customizable user avatars.
- Enhanced the DocsGridActions component to include a share option,
allowing users to share documents easily.
- Refactored SVG assets for pinned and simple documents to improve their
dimensions and visual representation.
- Improved the SimpleDocItem component to display document update times
and access indicators more effectively.
- Adjusted padding and spacing across various components to enhance
overall user experience.
2025-01-13 11:02:24 +01:00
Nathan Panchout
72f234027c (frontend) enhance left panel components
- Updated padding and radius styles in LeftPanelTargetFilters and
LeftPanelFavorites for improved layout consistency.
- Introduced LeftPanelDocContent component to display document details
when navigating to specific documentation pages.
- Enhanced LeftPanelContent to conditionally render LeftPanelDocContent
based on the current route.
- Adjusted LeftPanelHeader button colors for better visual hierarchy.
- Refactored MainLayout padding for a more responsive design.
2025-01-13 11:02:24 +01:00
Nathan Panchout
730efe7b74 (frontend) update color tokens and styles
- Modified color tokens for danger and info categories to enhance visual
consistency and accessibility.
- Updated button and modal styles, including adjustments to padding and
dimensions for improved layout.
- Replaced font files for Marianne with updated versions to ensure
better typography.
2025-01-13 11:02:24 +01:00
Nathan Panchout
63885117e1 (frontend) implement document favorites feature
- Added functionality to mark documents as favorites, including new
hooks `useMakeFavoriteDoc` and `useRemoveFavoriteDoc` for managing
favorite status.
- Enhanced the document management API to support favorite filtering
with the `is_favorite` parameter.
- Created a new e2e test for the favorite workflow to ensure proper
functionality.
- Updated the UI components to reflect favorite status, including
changes in `DocsGridActions`, `DocsGridItem`, and the new
`LeftPanelFavorites` component for displaying pinned documents.
- Adjusted SVG assets for better visual representation of pinned
documents.
2025-01-13 11:02:24 +01:00
Nathan Panchout
4f4c8905ff (frontend) update tests
- Enhanced test coverage for document sharing, member roles, and
visibility settings
2025-01-13 11:02:24 +01:00
Nathan Panchout
a5f6cb542d 🔥(frontend) remove unused document management components
- Deleted `DocVisibility`, `ModalShare`, `InvitationList`, `MemberList`,
and related components to streamline the document management feature.
- Updated component exports to reflect the removal of these components.
- Cleaned up associated assets and styles to improve code
maintainability.
2025-01-13 11:02:24 +01:00
Nathan Panchout
8456f47260 (frontend) enhance dropdown components and add new LoadMoreText feature
- Updated DropButton and DropdownMenu components to include new props
for accessibility and improved layout.
- Introduced LoadMoreText component for better user experience in
loading additional content.
- Added SearchUserRow and UserAvatar components for improved user search
functionality.
- Cleaned up unused imports and adjusted styles for better consistency
across components.
2025-01-13 11:02:24 +01:00
Nathan Panchout
eb35fdc7a9 (frontend) enhance document sharing features and role management
- Introduced new hooks and components for improved document sharing
functionality, including `useTranslatedShareSettings` and
`DocShareModal`.
- Added role management capabilities with `DocRoleDropdown` and
`DocShareAddMemberList` components, allowing users to manage document
access and roles effectively.
- Implemented user invitation handling with `DocShareInvitationItem` and
`DocShareMemberItem` components, enhancing the user experience for
managing document collaborators.
- Updated translation handling for role and visibility settings to
ensure consistency across the application.
- Refactored existing components to integrate new features and improve
overall code organization.
2025-01-13 11:02:24 +01:00
Nathan Panchout
ceaf1e28f9 (frontend) refactor QuickSearch components
- Simplified QuickSearchProps by removing unused properties and
enhancing type definitions.
- Updated QuickSearch component to utilize children for rendering,
improving flexibility.
- Added separator prop to QuickSearchInput for better control over
layout.
- Removed data prop from DocSearchModal's QuickSearch to streamline the
component's usage.
2025-01-13 11:02:24 +01:00
Nathan Panchout
c3da23f5d3 (frontend) add new color tokens and utility classes
- Introduced a comprehensive set of color tokens for blue, brown, cyan,
gold, green, olive, orange, pink, purple, and yellow shades.
2025-01-13 11:02:24 +01:00
Nathan Panchout
44784b2236 (frontend) implement document search functionality
- Added a new DocSearchModal component for searching documents.
- Introduced DocSearchItem component to display individua
 document results.
- Enhanced the useDocs API to support title-based searching.
- Implemented e2e tests for document search visibility and
functionality.
- Included an empty state illustration for no search results.
- Updated the LeftPanelHeader to open the document search modal.
2025-01-13 11:02:24 +01:00
Nathan Panchout
157f6200f2 (frontend) add Quick Search component suite
- Introduced a new Quick Search feature with multiple components
- Implemented styling for the Quick Search components to
ensure a cohesive look and feel across the application.
2025-01-13 11:02:24 +01:00
Nathan Panchout
2882348547 (frontend) update dependencies and enhance package configurations
- Added new dependencies: `luxon` and its type definitions
to the e2e app
- Introduced `cmdk` and `use-debounce` to the impress
app for enhanced UI components and debouncing functionality.
2025-01-13 11:02:24 +01:00
Nathan Panchout
e016cfab70 🐛(frontend) fix document editor height and update translations
- Adjusted the document editor height in the DocEditor component
- Updated translations for various terms to ensure consistency
cross the application.
- Improved layout and spacing in the DocsGridItem
component for a cleaner presentation.
2025-01-13 11:02:24 +01:00
Nathan Panchout
23b11e4096 💄(frontend) update document summary UI
- Enhanced the document summary UI for better visibility
and interaction.
- Refactored the DocHeader and DocEditor components to
 improve layout and responsiveness.
- Updated tests for the DocTableContent to reflect changes
in heading interactions and visibility checks.
2025-01-13 11:02:24 +01:00
Nathan Panchout
7696872416 (frontend) implement document filtering
- Introduced a new enum for default document filters
to improve code clarity.
- Updated the API call to support filtering documents
based on the creator.
- Enhanced the DocsGrid component to accept a target
filter, allowing dynamic content rendering based on user selection.
- Modified the main layout to include a left panel for improved
navigation and user experience.
- Added a new test suite for document filters, verifying the visibility
and selection states of 'All docs', 'My docs', and 'Shared with me'.
2025-01-13 11:02:24 +01:00
Nathan Panchout
42d9fa70a2 🔧(frontend) remove deprecated routes and update service worker
- Removed the versioning route from the default configuration to
streamline the documentation structure.
- Updated the service worker to eliminate references to the deprecated
 versioning fallback, enhancing the offline experience for users.
2025-01-13 11:02:24 +01:00
Nathan Panchout
a8a89def98 (frontend) enhance document versioning and loading experience
- Updated tests for document member list and versioning to utilize
'Load more' button instead of mouse wheel scrolling.
- Improved UI for document versioning, including visibility
checks and modal interactions.
- Refactored InfiniteScroll component to include a button for
loading more items, enhancing user experience.
- Adjusted DocEditor and DocHeader components to handle
version IDs more effectively.
- Removed deprecated versioning pages to streamline the codebase.
2025-01-13 11:02:24 +01:00
Nathan Panchout
5bcce0c64a 💄(frontend) update doc export modal
In the new interface, the export modal changes a little.

- We put the buttons on the right
- We remove the alert
- We transform the radio into select
2025-01-13 11:02:24 +01:00
Nathan Panchout
3a738fe701 (frontend) adapt all tests related to the new header
Since we no longer use an editable div but an input, we must
modify the tests accordingly
2025-01-13 11:02:24 +01:00
Nathan Panchout
d5670640f5 (frontend) update doc header ui
Modification of the header style to be consistent with the new UI :
- We replace the option menu with the DropdownMenu component
- We add a dowload button
- We put an input in place of an editable div.
2025-01-13 11:02:24 +01:00
Nathan Panchout
1d85eee78f 💄(frontend) add dropdown option for DocGridItem
Implement dropdown menu with functionality to delete a document
from the list
2025-01-13 11:02:24 +01:00
Nathan Panchout
5a46ab0055 (frontend) update tests to align with the new interface changes
- Adjust selectors and assertions to reflect updates in the UI layout and
design.
- Ensure all modified tests maintain compatibility with the updated structure.
- Fix any broken test cases caused by the redesign.
2025-01-13 11:02:24 +01:00
Nathan Panchout
3d5ff93a51 💄(frontend) update DocsGrid component
Implement the new version of  the DocsGrid  component
2025-01-13 11:02:24 +01:00
Nathan Panchout
b9c66c7c2a 🔧(frontend) update cunningham configuration
- update primary colors,and spacing.
- update tertiary button
2025-01-13 11:02:24 +01:00
Nathan Panchout
68a390ef59 (frontend) add react-intersection-observer package
- Install `react-intersection-observer` to manage element visibility detection.
- Enables features like lazy loading, animations on scroll, and triggering
events when elements appear in the viewport.
2025-01-13 11:02:24 +01:00
Nathan Panchout
192ab1121c 🔥(frontend) remove unused components due to new interface
Deleted two components that were no longer needed following the
implementation of the new interface. This cleanup helps streamline
he codebase and avoid unnecessary maintenance.
2025-01-13 11:02:24 +01:00
Nathan Panchout
83eb33d54a 💄(frontend) updating the header and leftpanel for responsive
Previously we added a left panel. We now need to adapt the layout
so that it becomesresponsive.

We therefore add a burger menu on the left on mobile which,
when clicked, deploys the left-panel over all the content.
2025-01-13 11:02:24 +01:00
Nathan Panchout
ee937de2c4 (frontend) update tests
Some minor changes have been integrated into the list of documents.
The tests must therefore be adapted accordingly.
2025-01-13 11:02:24 +01:00
Nathan Panchout
8d514bd571 💄(frontend) add left panel
In the new interface there is a new left panel. We implement it and add it
to the MainLayout
2025-01-13 11:02:24 +01:00
Nathan Panchout
e83c404e21 💄(frontend) add cunningham tokens
In order to use the spaces and grays of the DSFR,
we update the cunningham.ts file
2025-01-13 11:02:24 +01:00
Samuel Paccoud - DINUM
945f55f50d (backend) add test to secure updating user when matched on email
We had doubts that the user was correctly updated in the case where
its identity was matched on the email and not on the sub. I added
a test and confirmed that it was working correctly. I still modified
the backend to update the user based on its "id" instead of its "sub"
because it was confusing, but both actually work the same.
2025-01-10 19:30:17 +01:00
Samuel Paccoud - DINUM
9f83ea7111 ♻️(backend) rename required claims to essential claims as per spec
It was pointed by @lebaudantoine that the OIDC specification uses
the term "essential claims" for what we called required claims.

Further more, the Mozilla OIDC library that we use, validates claims
in a method called "verify_claims". Let's override this method.
2025-01-10 19:30:17 +01:00
Jacques ROUSSEL
f12c06e975 🔧(helm) add option to configure deployment annotations
We need to be abble to add specific annotations on Deployment in order
to use a reloader when external-secret sync new secrets
2025-01-09 07:20:01 +01:00
Jacques ROUSSEL
bbb176e153 🐛(CI) fix ci
The backend secret is managed by external-secret now so we should not
keep it in the chart
2025-01-08 11:55:40 +01:00
Jacques ROUSSEL
02793040fd 🐛(CI) improve helm release
In order to avoid a github release when we build the helm chart, we use
another action
2025-01-06 15:44:16 +01:00
Jacques ROUSSEL
0773e83149 📝(self-hosted) add documentation
Add a documentation to deploy a self-hosted visio instance in a
standalone way (witout AI features)
2025-01-06 15:44:16 +01:00
Jacques ROUSSEL
21205b4d19 🐛(CI) add helm release action
In order to avoird code duplication we have to release a helm chart
2025-01-06 15:44:16 +01:00
Jacques ROUSSEL
60dbf6c11d 💚(ci) fix jobs after migration
The repository migration broke the CI. To fix it, we removed the
dependency on the secrets repository.
2025-01-06 12:17:40 +01:00
Anthony LC
2491ad7142 (e2e) adapt test to new Blocknote release
Copy as html provide a html lightly different than
before, so the test was adapted to the new html
provided.
2025-01-02 15:42:39 +01:00
Anthony LC
3b2834cf6d ⬆️(dependencies) update js dependencies 2025-01-02 15:42:39 +01:00
renovate[bot]
7ed2b23ea3 ⬆️(dependencies) update python dependencies 2024-12-30 11:00:32 +01:00
Samuel Paccoud - DINUM
c879f82114 (backend) add option to configure list of required OIDC claims
We want to be able to refuse connection for users who have missing
claims from a list of required keys.
2024-12-24 17:10:52 +01:00
Anthony LC
02a4740c66 ♻️(frontend) create useProviderStore
We created useProviderStore, a store dedicated
to managing the provider of the document.
We created as well a new hook useCollaboration,
it will be use to interact with the provider store.
This refacto is a first step to implement
the long polling.
2024-12-24 12:29:30 +01:00
Anthony LC
6cb2702e6b ♻️(frontend) update already existing tasks
When a task was already existing, we were not
updating it. This commit fixes that.
2024-12-24 12:29:30 +01:00
Anthony LC
94a9f7a84e 🔒️(y-provider) add cors middlewares
Add cors middlewares to y-provider server.
It will control how clients connect to the server
with http requests.
2024-12-24 12:29:30 +01:00
Anthony LC
e53465ce11 🏗️(y-provider) organize yjs server
Many routes were in the server.ts file, now they
are in their own files in the handlers folder.
The server.ts file is now AppServer that handles
the routes.
We split as well the tests.
2024-12-24 12:29:30 +01:00
676 changed files with 51655 additions and 19047 deletions

View File

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

View File

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

View File

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

77
.github/workflows/crowdin_download.yml vendored Normal file
View File

@@ -0,0 +1,77 @@
name: Download translations from Crowdin
on:
workflow_dispatch:
push:
branches:
- 'release/**'
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
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

76
.github/workflows/crowdin_upload.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: Update crowdin sources
on:
workflow_dispatch:
push:
branches:
- main
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
with-build_mails: true
synchronize-with-crowdin:
needs: install-dependencies
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.13.3"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
run: pip install --user .
working-directory: src/backend
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
fail-on-cache-miss: true
- name: Install gettext
run: |
sudo apt-get update
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/"

85
.github/workflows/dependencies.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: Dependency reusable workflow
on:
workflow_call:
inputs:
node_version:
required: false
default: '20.x'
type: string
with-front-dependencies-installation:
type: boolean
default: false
with-build_mails:
type: boolean
default: false
jobs:
front-dependencies-installation:
if: ${{ inputs.with-front-dependencies-installation == true }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore the frontend cache
uses: actions/cache@v4
id: front-node_modules
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
- name: Setup Node.js
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Install dependencies
if: steps.front-node_modules.outputs.cache-hit != 'true'
run: cd src/frontend/ && yarn install --frozen-lockfile
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
build-mails:
if: ${{ inputs.with-build_mails == true }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Setup Node.js
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g yarn
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}

View File

@@ -5,12 +5,7 @@ on:
workflow_dispatch:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'
- 'do-not-merge/hackathon-2025'
env:
DOCKER_USER: 1001:127
@@ -19,26 +14,9 @@ jobs:
build-and-push-backend:
runs-on: ubuntu-latest
steps:
-
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "impress,secrets"
-
name: Checkout repository
uses: actions/checkout@v2
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Load sops secrets
uses: rouja/actions-sops@main
with:
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
uses: actions/checkout@v4
-
name: Docker meta
id: meta
@@ -47,49 +25,30 @@ jobs:
images: lasuite/impress-backend
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '--target backend-production -f Dockerfile'
docker-image-name: 'docker.io/lasuite/impress-backend:${{ github.sha }}'
continue-on-error: true
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: .
target: backend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-frontend:
runs-on: ubuntu-latest
steps:
-
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "impress,secrets"
-
name: Checkout repository
uses: actions/checkout@v2
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Load sops secrets
uses: rouja/actions-sops@main
with:
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
uses: actions/checkout@v4
-
name: Docker meta
id: meta
@@ -98,50 +57,33 @@ jobs:
images: lasuite/impress-frontend
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
continue-on-error: true
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
PUBLISH_AS_MIT=false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-y-provider:
runs-on: ubuntu-latest
steps:
-
uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "impress,secrets"
-
name: Checkout repository
uses: actions/checkout@v2
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Load sops secrets
uses: rouja/actions-sops@main
with:
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
uses: actions/checkout@v4
-
name: Docker meta
id: meta
@@ -150,24 +92,45 @@ jobs:
images: lasuite/impress-y-provider
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f src/frontend/servers/y-provider/Dockerfile --target y-provider'
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
continue-on-error: true
docker-image-name: 'docker.io/lasuite/impress-y-provider:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./src/frontend/servers/y-provider/Dockerfile
target: y-provider
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-mcp-server:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: lasuite/impress-mcp-server
- name: Login to DockerHub
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: ./src/mcp_server
file: ./src/mcp_server/Dockerfile
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -176,32 +139,10 @@ jobs:
- build-and-push-frontend
- build-and-push-backend
runs-on: ubuntu-latest
if: |
github.event_name != 'pull_request'
steps:
-
uses: actions/create-github-app-token@v1
id: app-token
- uses: numerique-gouv/action-argocd-webhook-notification@main
id: notify
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "impress,secrets"
-
name: Checkout repository
uses: actions/checkout@v2
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
-
name: Load sops secrets
uses: rouja/actions-sops@main
with:
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
-
name: Call argocd github webhook
run: |
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
argocd_webhook_secret: "${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET }}"
argocd_url: "${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}"

View File

@@ -2,6 +2,7 @@ name: Helmfile lint
run-name: Helmfile lint
on:
push:
pull_request:
branches:
- 'main'
@@ -10,13 +11,20 @@ jobs:
helmfile-lint:
runs-on: ubuntu-latest
container:
image: ghcr.io/helmfile/helmfile:latest
image: ghcr.io/helmfile/helmfile:v0.171.0
steps:
-
uses: numerique-gouv/action-helmfile-lint@main
with:
app-id: ${{ secrets.APP_ID }}
age-key: ${{ secrets.SOPS_PRIVATE }}
private-key: ${{ secrets.PRIVATE_KEY }}
helmfile-src: "src/helm"
repositories: "impress,secrets"
-
name: Checkout repository
uses: actions/checkout@v4
-
name: Helmfile lint
shell: bash
run: |
set -e
HELMFILE=src/helm/helmfile.yaml
environments=$(awk 'BEGIN {in_env=0} /^environments:/ {in_env=1; next} /^---/ {in_env=0} in_env && /^ [^ ]/ {gsub(/^ /,""); gsub(/:.*$/,""); print}' "$HELMFILE")
for env in $environments; do
echo "################### $env lint ###################"
helmfile -e $env -f $HELMFILE lint || exit 1
echo -e "\n"
done

View File

@@ -9,39 +9,16 @@ 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') }}
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
test-front:
needs: install-dependencies
runs-on: ubuntu-latest
needs: install-front
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -53,44 +30,54 @@ 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
lint-front:
runs-on: ubuntu-latest
needs: install-front
needs: install-dependencies
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: Check linting
run: cd src/frontend/ && yarn lint
test-e2e-chromium:
runs-on: ubuntu-latest
needs: install-dependencies
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
@@ -101,28 +88,6 @@ jobs:
- name: Start Docker services
run: make bootstrap FLUSH_ARGS='--no-input' cache=
# Tool to wait for a service to be ready
- name: Install Dockerize
run: |
curl -sSL https://github.com/jwilder/dockerize/releases/download/v0.8.0/dockerize-linux-amd64-v0.8.0.tar.gz | sudo tar -C /usr/local/bin -xzv
- name: Wait for services to be ready
run: |
printf "Minio check...\n"
dockerize -wait tcp://localhost:9000 -timeout 20s
printf "Keyclock check...\n"
dockerize -wait tcp://localhost:8080 -timeout 20s
printf "Server collaboration check...\n"
dockerize -wait tcp://localhost:4444 -timeout 20s
printf "Ngnix check...\n"
dockerize -wait tcp://localhost:8083 -timeout 20s
printf "DRF check...\n"
dockerize -wait tcp://localhost:8071 -timeout 20s
printf "Postgres Keyclock check...\n"
dockerize -wait tcp://localhost:5433 -timeout 20s
printf "Postgres back check...\n"
dockerize -wait tcp://localhost:15432 -timeout 20s
- name: Run e2e tests
run: cd src/frontend/ && yarn e2e:test --project='chromium'
@@ -141,12 +106,17 @@ 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: Set e2e env variables
run: cat env.d/development/common.e2e.dist >> env.d/development/common.dist

View File

@@ -9,6 +9,11 @@ on:
- "*"
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
with-build_mails: true
lint-git:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests
@@ -56,45 +61,24 @@ jobs:
exit 1
fi
build-mails:
lint-spell-mistakes:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/mail
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g yarn
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
uses: actions/checkout@v2
- name: Install codespell
run: pip install --user codespell
- name: Check for typos
run: |
codespell \
--check-filenames \
--ignore-words-list "Dokument,afterAll,excpt,statics" \
--skip "./git/" \
--skip "**/*.po" \
--skip "**/*.pot" \
--skip "**/*.json" \
--skip "**/yarn.lock"
lint-back:
runs-on: ubuntu-latest
@@ -107,7 +91,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.12.6"
python-version: "3.13.3"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
@@ -121,7 +105,7 @@ jobs:
test-back:
runs-on: ubuntu-latest
needs: build-mails
needs: install-dependencies
defaults:
run:
@@ -169,6 +153,7 @@ jobs:
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
fail-on-cache-miss: true
- name: Start MinIO
run: |
@@ -201,15 +186,16 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.12.6"
python-version: "3.13.3"
- name: Install development dependencies
run: pip install --user .[dev]
- 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

View File

@@ -0,0 +1,34 @@
name: Release Chart
run-name: Release Chart
on:
push:
paths:
- src/helm/impress/**
jobs:
release:
# depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
# see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cleanup
run: rm -rf ./src/helm/extra
- name: Install Helm
uses: azure/setup-helm@v4
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Publish Helm charts
uses: numerique-gouv/helm-gh-pages@add-overwrite-option
with:
charts_dir: ./src/helm
token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ MANIFEST
.next/
# Translations # Translations
*.mo
*.pot
# Environments

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "secrets"]
path = secrets
url = ../secrets

View File

@@ -6,13 +6,287 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [3.3.0] - 2025-05-06
### Added
- ✨(backend) add endpoint checking media status #984
- ✨(backend) allow setting session cookie age via env var #977
- ✨(backend) allow theme customnization using a configuration file #948
- ✨(frontend) Add a custom callout block to the editor #892
- 🚩(frontend) version MIT only #911
- ✨(backend) integrate maleware_detection from django-lasuite #936
- 🏗️(frontend) Footer configurable #959
- 🩺(CI) add lint spell mistakes #954
- ✨(frontend) create generic theme #792
- 🛂(frontend) block edition to not connected users #945
- 🚸(frontend) Let loader during upload analyze #984
- 🚩(frontend) feature flag on blocking edition #997
### Changed
- 📝(frontend) Update documentation #949
- ✅(frontend) Improve tests coverage #949
- ⬆️(docker) upgrade backend image to python 3.13 #973
- ⬆️(docker) upgrade node images to alpine 3.21 #973
### Fixed
- 🐛(y-provider) increase JSON size limits for transcription conversion #989
### Removed
- 🔥(back) remove footer endpoint #948
## [3.2.1] - 2025-05-06
## Fixed
- 🐛(frontend) fix list copy paste #943
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
## [3.2.0] - 2025-05-05
## Added
🔧(helm) add option to disable default tls setting by @dominikkaminski #519
- 🚸(backend) make document search on title accent-insensitive #874
- 🚩 add homepage feature flag #861
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
- ✨(settings) Allow configuring PKCE for the SSO #886
- 🌐(i18n) activate chinese and spanish languages #884
- 🔧(backend) allow overwriting the data directory #893
- (backend) add `django-lasuite` dependency #839
- ✨(frontend) advanced table features #908
## Changed
- ⚡️(frontend) reduce unblocking time for config #867
- ♻️(frontend) bind UI with ability access #900
- ♻️(frontend) use built-in Quote block #908
## Fixed
- 🐛(nginx) fix 404 when accessing a doc #866
- 🔒️(drf) disable browsable HTML API renderer #919
- 🔒(frontend) enhance file download security #889
- 🐛(backend) race condition create doc #633
- 🐛(frontend) fix breaklines in custom blocks #908
## [3.1.0] - 2025-04-07
## Added
- 🚩(backend) add feature flag for the footer #841
- 🔧(backend) add view to manage footer json #841
- ✨(frontend) add custom css style #771
- 🚩(frontend) conditionally render AI button only when feature is enabled #814
## Changed
- 🚨(frontend) block button when creating doc #749
## Fixed
- 🐛(back) validate document content in serializer #822
- 🐛(frontend) fix selection click past end of content #840
## [3.0.0] - 2025-03-28
## Added
- 📄(legal) Require contributors to sign a DCO #779
## Changed
- ♻️(frontend) Integrate UI kit #783
- 🏗️(y-provider) manage auth in y-provider app #804
## Fixed
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
- 🔒️(back) restrict access to document accesses #801
## [2.6.0] - 2025-03-21
## Added
- 📝(doc) add publiccode.yml #770
## Changed
- 🚸(frontend) ctrl+k modal not when editor is focused #712
## Fixed
- 🐛(back) allow only images to be used with the cors-proxy #781
- 🐛(backend) stop returning inactive users on the list endpoint #636
- 🔒️(backend) require at least 5 characters to search for users #636
- 🔒️(back) throttle user list endpoint #636
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
## [2.5.0] - 2025-03-18
## Added
- 📝(doc) Added GNU Make link to README #750
- ✨(frontend) add pinning on doc detail #711
- 🚩(frontend) feature flag analytic on copy as html #649
- ✨(frontend) Custom block divider with export #698
- 🌐(i18n) activate dutch language #742
- ✨(frontend) add Beautify action to AI transform #478
- ✨(frontend) add Emojify action to AI transform #478
## Changed
- 🧑‍💻(frontend) change literal section open source #702
- ♻️(frontend) replace cors proxy for export #695
- 🚨(gitlint) Allow uppercase in commit messages #756
- ♻️(frontend) Improve AI translations #478
## Fixed
- 🐛(frontend) SVG export #706
- 🐛(frontend) remove scroll listener table content #688
- 🔒️(back) restrict access to favorite_list endpoint #690
- 🐛(backend) refactor to fix filtering on children
and descendants views #695
- 🐛(action) fix notify-argocd workflow #713
- 🚨(helm) fix helmfile lint #736
- 🚚(frontend) redirect to 401 page when 401 error #759
## [2.4.0] - 2025-03-06
## Added
- ✨(frontend) synchronize language-choice #401
## Changed
- Use sentry tags instead of extra scope
## Fixed
- 🐛(frontend) fix collaboration error #684
## [2.3.0] - 2025-03-03
## Added
- ✨(backend) limit link reach/role select options depending on ancestors #645
- ✨(backend) add new "descendants" action to document API endpoint #645
- ✨(backend) new "tree" action on document detail endpoint #645
- ✨(backend) allow forcing page size within limits #645
- 💄(frontend) add error pages #643
- 🔒️ Manage unsafe attachments #663
- ✨(frontend) Custom block quote with export #646
- ✨(frontend) add open source section homepage #666
- ✨(frontend) synchronize language-choice #401
## Changed
- 🛂(frontend) Restore version visibility #629
- 📝(doc) minor README.md formatting and wording enhancements
-Stop setting a default title on doc creation #634
- ♻️(frontend) misc ui improvements #644
## Fixed
- 🐛(backend) allow any type of extensions for media download #671
- ♻️(frontend) improve table pdf rendering
- 🐛(email) invitation emails in receivers language
## [2.2.0] - 2025-02-10
## Added
- 📝(doc) Add security.md and codeofconduct.md #604
- ✨(frontend) add home page #608
- ✨(frontend) cursor display on activity #609
- ✨(frontend) Add export page break #623
## Changed
- 🔧(backend) make AI feature reach configurable #628
## Fixed
- 🌐(CI) Fix email partially translated #616
- 🐛(frontend) fix cursor breakline #609
- 🐛(frontend) fix style pdf export #609
## [2.1.0] - 2025-01-29
## Added
- ✨(backend) add duplicate action to the document API endpoint
- ⚗️(backend) add util to extract text from base64 yjs document
- ✨(backend) add soft delete and restore API endpoints to documents #516
- ✨(backend) allow organizing documents in a tree structure #516
- ✨(backend) add "excerpt" field to document list serializer #516
- ✨(backend) add github actions to manage Crowdin workflow #559 & #563
- 📈Integrate Posthog #540
- 🏷️(backend) add content-type to uploaded files #552
- ✨(frontend) export pdf docx front side #537
## Changed
- 💄(frontend) add abilities on doc row #581
- 💄(frontend) improve DocsGridItem responsive padding #582
- 🔧(backend) Bump maximum page size to 200 #516
- 📝(doc) Improve Read me #558
## Fixed
- 🐛Fix invitations #575
## Removed
- 🔥(backend) remove "content" field from list serializer # 516
## [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
- 💄(frontend) add filtering to left panel #475
- ✨(frontend) new share modal ui #489
- ✨(frontend) add favorite feature #515
- 📝(documentation) Documentation about self-hosted installation #530
- ✨(helm) helm versioning #530
## Changed
- 🏗️(yjs-server) organize yjs server #528
- ♻️(frontend) better separation collaboration process #528
- 💄(frontend) updating the header and leftpanel for responsive #421
- 💄(frontend) update DocsGrid component #431
- 💄(frontend) update DocsGridOptions component #432
- 💄(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 & #572
## 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
@@ -30,6 +304,10 @@ and this project adheres to
- ⚡️(e2e) reduce flakiness on e2e tests #511
## Fixed
- 🐛(frontend) update doc editor height #481
- 💄(frontend) add doc search #485
## [1.9.0] - 2024-12-11
@@ -52,21 +330,18 @@ and this project adheres to
- 🐛(frontend) Fix hidden menu on Firefox #468
- 🐛(backend) fix sanitize problem IA #490
## [1.8.2] - 2024-11-28
## Changed
- ♻️(SW) change strategy html caching #460
## [1.8.1] - 2024-11-27
## Fixed
- 🐛(frontend) link not clickable and flickering firefox #457
## [1.8.0] - 2024-11-25
## Added
@@ -95,7 +370,6 @@ and this project adheres to
- 🐛(frontend) users have view access when revoked #387
- 🐛(frontend) fix placeholder editable when double clicks #454
## [1.7.0] - 2024-10-24
## Added
@@ -123,7 +397,6 @@ and this project adheres to
- 🔥(helm) remove infra related codes #366
## [1.6.0] - 2024-10-17
## Added
@@ -146,7 +419,6 @@ and this project adheres to
- 🐛(backend) fix nginx docker container #340
- 🐛(frontend) fix copy paste firefox #353
## [1.5.1] - 2024-10-10
## Fixed
@@ -181,7 +453,6 @@ and this project adheres to
- 🔧(backend) fix configuration to avoid different ssl warning #297
- 🐛(frontend) fix editor break line not working #302
## [1.4.0] - 2024-09-17
## Added
@@ -202,7 +473,6 @@ and this project adheres to
- 🐛(backend) Fix forcing ID when creating a document via API endpoint #234
- 🐛 Rebuild frontend dev container from makefile #248
## [1.3.0] - 2024-09-05
## Added
@@ -227,7 +497,6 @@ and this project adheres to
- 🔥(frontend) remove saving modal #213
## [1.2.1] - 2024-08-23
## Changed
@@ -235,7 +504,6 @@ and this project adheres to
- ♻️ Change ordering docs datagrid #195
- 🔥(helm) use scaleway email #194
## [1.2.0] - 2024-08-22
## Added
@@ -259,14 +527,14 @@ and this project adheres to
- ⚡️(CI) only e2e chrome mandatory #177
## Removed
- 🔥(helm) remove htaccess #181
- 🔥(helm) remove htaccess #181
## [1.1.0] - 2024-07-15
## Added
- 🤡(demo) generate dummy documents on dev users #120
- 🤡(demo) generate dummy documents on dev users #120
- ✨(frontend) create side modal component #134
- ✨(frontend) Doc grid actions (update / delete) #136
- ✨(frontend) Doc editor header information #137
@@ -277,12 +545,11 @@ and this project adheres to
- ♻️(frontend) create a doc from a modal #132
- ♻️(frontend) manage members from the share modal #140
## [1.0.0] - 2024-07-02
## Added
- 🛂(frontend) Manage the document's right (#75)
- 🛂(frontend) Manage the document's right (#75)
- ✨(frontend) Update document (#68)
- ✨(frontend) Remove document (#68)
- 🐳(docker) dockerize dev frontend (#63)
@@ -305,7 +572,7 @@ and this project adheres to
- ⚡️(e2e) unique login between tests (#80)
- ⚡️(CI) improve e2e job (#86)
- ♻️(frontend) improve the error and message info ui (#93)
- ✏️(frontend) change all occurences of pad to doc (#99)
- ✏️(frontend) change all occurrences of pad to doc (#99)
## Fixed
@@ -316,7 +583,6 @@ and this project adheres to
- 💚(CI) Remove trigger workflow on push tags on CI (#68)
- 🔥(frontend) Remove coming soon page (#121)
## [0.1.0] - 2024-05-24
## Added
@@ -324,8 +590,20 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.10.0...main
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.3.0...main
[v3.3.0]: https://github.com/numerique-gouv/impress/releases/v3.3.0
[v3.2.1]: https://github.com/numerique-gouv/impress/releases/v3.2.1
[v3.2.0]: https://github.com/numerique-gouv/impress/releases/v3.2.0
[v3.1.0]: https://github.com/numerique-gouv/impress/releases/v3.1.0
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
[v2.6.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0
[v2.4.0]: https://github.com/numerique-gouv/impress/releases/v2.4.0
[v2.3.0]: https://github.com/numerique-gouv/impress/releases/v2.3.0
[v2.2.0]: https://github.com/numerique-gouv/impress/releases/v2.2.0
[v2.1.0]: https://github.com/numerique-gouv/impress/releases/v2.1.0
[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

79
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,79 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
- Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at docs@numerique.gouv.fr.
- All complaints will be reviewed and investigated promptly and fairly.
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of the following Code of Conduct
## Code of Conduct:
### 1. Correction
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozilla's [code of conduct enforcement ladder](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md).
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

View File

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

View File

@@ -1,7 +1,7 @@
# Django impress
# ---- base image to inherit from ----
FROM python:3.12.6-alpine3.20 AS base
FROM python:3.13.3-alpine AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip setuptools
@@ -15,6 +15,13 @@ FROM base AS back-builder
WORKDIR /builder
# Install Rust and Cargo using Alpine's package manager
RUN apk add --no-cache \
build-base \
libffi-dev \
rust \
cargo
# Copy required python dependencies
COPY ./src/backend /builder
@@ -23,7 +30,7 @@ RUN mkdir /install && \
# ---- mails ----
FROM node:20 AS mail-builder
FROM node:24 AS mail-builder
COPY ./src/mail /mail/app
@@ -51,7 +58,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
@@ -72,10 +79,11 @@ RUN apk add \
gettext \
gdk-pixbuf \
libffi-dev \
pandoc \
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 +100,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.
@@ -126,6 +139,9 @@ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# ---- Production image ----
FROM core AS backend-production
# Remove apk cache, we don't need it anymore
RUN rm -rf /var/cache/apk/*
ARG IMPRESS_STATIC_ROOT=/data/static
# Gunicorn

View File

@@ -44,7 +44,6 @@ 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
@@ -81,12 +80,12 @@ bootstrap: \
data/static \
create-env-files \
build \
run-with-frontend \
migrate \
demo \
back-i18n-compile \
mails-install \
mails-build
mails-build \
run
.PHONY: bootstrap
# -- Docker/compose
@@ -109,7 +108,7 @@ build-yjs-provider: ## build the y-provider container
build-frontend: cache ?=
build-frontend: ## build the frontend container
@$(COMPOSE) build frontend-dev $(cache)
@$(COMPOSE) build frontend $(cache)
.PHONY: build-frontend
down: ## stop and remove containers, networks, images, and volumes
@@ -120,18 +119,17 @@ logs: ## display app-dev logs (follow mode)
@$(COMPOSE) logs -f app-dev
.PHONY: logs
run: ## start the wsgi (production) and development server
run-backend: ## Start only the backend application and all needed services
@$(COMPOSE) up --force-recreate -d celery-dev
@$(COMPOSE) up --force-recreate -d y-provider
@$(COMPOSE) up --force-recreate -d nginx
@echo "Wait for postgresql to be up..."
@$(WAIT_DB)
.PHONY: run
.PHONY: run-backend
run-with-frontend: ## Start all the containers needed (backend to frontend)
@$(MAKE) run
@$(COMPOSE) up --force-recreate -d frontend-dev
.PHONY: run-with-frontend
run: ## start the wsgi (production) and development server
run:
@$(MAKE) run-backend
@$(COMPOSE) up --force-recreate -d frontend
.PHONY: run
status: ## an alias for "docker compose ps"
@$(COMPOSE) ps
@@ -188,14 +186,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
@@ -310,16 +306,16 @@ help:
.PHONY: help
# Front
frontend-install: ## install the frontend locally
frontend-development-install: ## install the frontend locally
cd $(PATH_FRONT_IMPRESS) && yarn
.PHONY: frontend-install
.PHONY: frontend-development-install
frontend-lint: ## run the frontend linter
cd $(PATH_FRONT) && yarn lint
.PHONY: frontend-lint
run-frontend-development: ## Run the frontend in development mode
@$(COMPOSE) stop frontend-dev
@$(COMPOSE) stop frontend
cd $(PATH_FRONT_IMPRESS) && yarn dev
.PHONY: run-frontend-development

212
README.md
View File

@@ -1,113 +1,213 @@
# Impress
<p align="center">
<a href="https://github.com/suitenumerique/docs">
<img alt="Docs" src="/docs/assets/banner-docs.png" width="100%" />
</a>
</p>
<p align="center">
<a href="https://github.com/suitenumerique/docs/stargazers/">
<img src="https://img.shields.io/github/stars/suitenumerique/docs" alt="">
</a>
<a href='https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md'><img alt='PRs Welcome' src='https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shields'/></a>
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/suitenumerique/docs"/>
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/suitenumerique/docs"/>
<a href="https://github.com/suitenumerique/docs/blob/main/LICENSE">
<img alt="GitHub closed issues" src="https://img.shields.io/github/license/suitenumerique/docs"/>
</a>
</p>
<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> - <a href="mailto:docs@numerique.gouv.fr">
Reach out
</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
# La Suite Docs : Collaborative Text Editing
Docs, where your notes can become knowledge through live collaboration.
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/)
<img src="/docs/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
## Getting started
## Why use Docs ❓
Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing.
### Prerequisite
It offers a scalable and secure alternative to tools such as Google Docs, Notion (without the dbs), Outline, or Confluence.
Make sure you have a recent version of Docker and [Docker
Compose](https://docs.docker.com/compose/install) installed on your laptop:
### Write
* 😌 Get simple, accessible online editing for your team.
* 💅 Create clean documents with beautiful formatting options.
* 🖌️ Focus on your content using either the in-line editor, or [the Markdown syntax](https://www.markdownguide.org/basic-syntax/).
* 🧱 Quickly design your page thanks to the many block types, accessible from the `/` slash commands, as well as keyboard shortcuts.
* 🔌 Write offline! Your edits will be synced once you're back online.
* ✨ Save time thanks to our AI actions, such as rephrasing, summarizing, fixing typos, translating, etc. You can even turn your selected text into a prompt!
```bash
### Work together
* 🤝 Enjoy live editing! See your team collaborate in real time.
* 🔒 Keep your information secure thanks to granular access control. Only share with the right people.
* 📑 Export your content in multiple formats (`.odt`, `.docx`, `.pdf`) with customizable templates.
* 📚 Turn your team's collaborative work into organized knowledge with Subpages.
### Self-host
🚀 Docs is easy to install on your own servers
Available methods: Helm chart, Nix package
In the works: Docker Compose, YunoHost
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.
## Getting started 🔧
### Test it
You can test Docs on your browser by visiting this [demo document](https://impress-preprod.beta.numerique.gouv.fr/docs/6ee5aac4-4fb9-457d-95bf-bb56c2467713/)
### Run Docs locally
> ⚠️ The methods described below for running Docs locally is **for testing purposes only**. It is based on building Docs using [Minio](https://min.io/) as an S3-compatible storage solution. Of course you can choose any S3-compatible storage solution.
**Prerequisite**
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop, then type:
```shellscript
$ docker -v
Docker version 20.10.2, build 2291f61
$ docker compose -v
docker compose version 1.27.4, build 40524192
Docker version 20.10.2, build 2291f61
$ docker compose version
Docker Compose version v2.32.4
```
> ⚠️ You may need to run the following commands with `sudo` but this can be
> avoided by assigning your user to the `docker` group.
> ⚠️ You may need to run the following commands with `sudo`, but this can be avoided by adding your user to the local `docker` group.
### Project bootstrap
**Project bootstrap**
The easiest way to start working on the project is to use GNU Make:
The easiest way to start working on the project is to use [GNU Make](https://www.gnu.org/software/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 compiles translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues.
Your Docker services should now be up and running 🎉
You can access to the project by going to http://localhost:3000.
You will be prompted to log in, the default credentials are:
```bash
You can access to the project by going to <http://localhost:3000>.
You will be prompted to log in. The default credentials are:
```
username: impress
password: impress
```
📝 Note that if you need to run them afterwards, you can use the eponym Make rule:
```bash
$ make run-with-frontend
```shellscript
$ make run
```
---
⚠️ For the frontend developer, 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
$ make frontend-install
```shellscript
$ make frontend-development-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
$ make run
```shellscript
$ make run-backend
```
---
**Adding content**
### Adding content
You can create a basic demo site by running this command:
You can create a basic demo site by running:
```shellscript
$ make demo
```
$ make demo
Finally, you can check all available Make rules using this command:
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).
You can access the Django admin site at:
<http://localhost:8071/admin>.
You first need to create a superuser account:
```bash
```shellscript
$ make superuser
```
## Contributing
## Feedback 🙋‍♂️🙋‍♀️
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.
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).
## License
## Roadmap
This work is released under the MIT License (see [LICENSE](./LICENSE)).
Want to know where the project is headed? [🗺️ Checkout our roadmap](https://github.com/orgs/numerique-gouv/projects/13/views/11)
## Licence 📝
This work is released under the MIT License (see [LICENSE](https://github.com/suitenumerique/docs/blob/main/LICENSE)).
While Docs is a public-driven initiative, our licence choice is an invitation for private sector actors to use, sell and contribute to the project.
## Contributing 🙌
This project is intended to be community-driven, so please, do not hesitate to [get in touch](https://matrix.to/#/#docs-official:matrix.org) if you have any question related to our implementation or design decisions.
You can help us with translations on [Crowdin](https://crowdin.com/project/lasuite-docs).
If you intend to make pull requests, see [CONTRIBUTING](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md) 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
Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/). We thank the contributors of all these projects for their awesome work!
We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/).
### Gov ❤️ open source
Docs is the result of a joint effort led by the French 🇫🇷🥖 ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German 🇩🇪🥨 governments ([ZenDiS](https://zendis.de/)).
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱🧀), feel free to [reach out](mailto:docs@numerique.gouv.fr) if you are interested in using or contributing to Docs.
<p align="center">
<img src="/docs/assets/europe_opensource.png" width="50%"/>
</p>

23
SECURITY.md Normal file
View File

@@ -0,0 +1,23 @@
# Security Policy
## Reporting a Vulnerability
Security is very important to us.
If you have any issue regarding security, please disclose the information responsibly submitting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at docs@numerique.gouv.fr
We appreciate your effort to make Docs more secure.
## Vulnerability disclosure policy
Working with security issues in an open source project can be challenging, as we are required to disclose potential problems that could be exploited by attackers. With this in mind, our security fix policy is as follows:
1. The Maintainers team will handle the fix as usual (Pull Request,
release).
2. In the release notes, we will include the identification numbers from the
GitHub Advisory Database (GHSA) and, if applicable, the Common Vulnerabilities
and Exposures (CVE) identifier for the vulnerability.
3. Once this grace period has passed, we will publish the vulnerability.
By adhering to this security policy, we aim to address security concerns
effectively and responsibly in our open source software project.

View File

@@ -15,3 +15,29 @@ the following command inside your docker container:
(Note : in your development environment, you can `make migrate`.)
## [Unreleased]
## [3.3.0] - 2025-05-22
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/docs/env.md) for more information.
The footer is now configurable from a customization file. To override the default one, you can
use the `THEME_CUSTOMIZATION_FILE_PATH` environment variable to point to your customization file.
The customization file must be a JSON file and must follow the rules described in the
[theming documentation](docs/theming.md).
## [3.0.0] - 2025-03-28
We are not using the nginx auth request anymore to access the collaboration server (`yProvider`)
The authentication is now managed directly from the yProvider server.
You must remove the annotation `nginx.ingress.kubernetes.io/auth-url` from the `ingressCollaborationWS`.
This means as well that the yProvider server must be able to access the Django server.
To do so, you must set the `COLLABORATION_BACKEND_BASE_URL` environment variable to the `yProvider`
service.
## [2.2.0] - 2025-02-10
- AI features are now limited to users who are authenticated. Before this release, even anonymous
users who gained editor access on a document with link reach used to get AI feature.
IF you want anonymous users to keep access on AI features, you must now define the
`AI_ALLOW_REACH_FROM` setting to "public".

View File

@@ -39,7 +39,19 @@ docker_build(
]
)
k8s_yaml(local('cd ../src/helm && helmfile -n impress -e dev template .'))
docker_build(
'localhost:5001/impress-mcp-server:latest',
context='../src/mcp_server',
dockerfile='../src/mcp_server/Dockerfile',
)
k8s_resource('impress-docs-backend-migrate', resource_deps=['postgres-postgresql'])
k8s_resource('impress-docs-backend-createsuperuser', resource_deps=['impress-docs-backend-migrate'])
k8s_resource('impress-docs-backend', resource_deps=['impress-docs-backend-migrate'])
# helmfile in docker mount the current working directory and the helmfile.yaml
# requires the keycloak config in another directory
k8s_yaml(local('cd .. && helmfile -n impress -e ${DEV_ENV:-dev} template --file ./src/helm/helmfile.yaml'))
migration = '''
set -eu

View File

@@ -7,7 +7,6 @@ UNSET_USER=0
TERRAFORM_DIRECTORY="./env.d/terraform"
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
COMPOSE_PROJECT="impress"
# _set_user: set (or unset) default user id used to run docker commands
@@ -40,9 +39,8 @@ function _set_user() {
# ARGS : docker compose command arguments
function _docker_compose() {
echo "🐳(compose) project: '${COMPOSE_PROJECT}' file: '${COMPOSE_FILE}'"
echo "🐳(compose) file: '${COMPOSE_FILE}'"
docker compose \
-p "${COMPOSE_PROJECT}" \
-f "${COMPOSE_FILE}" \
--project-directory "${REPO_DIR}" \
"$@"

View File

@@ -1,103 +1,2 @@
#!/bin/sh
set -o errexit
CURRENT_DIR=$(pwd)
echo "0. Create ca"
# 0. Create ca
mkcert -install
cd /tmp
mkcert "127.0.0.1.nip.io" "*.127.0.0.1.nip.io"
cd $CURRENT_DIR
echo "1. Create registry container unless it already exists"
# 1. Create registry container unless it already exists
reg_name='kind-registry'
reg_port='5001'
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
docker run \
-d --restart=unless-stopped -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \
registry:2
fi
echo "2. Create kind cluster with containerd registry config dir enabled"
# 2. Create kind cluster with containerd registry config dir enabled
# TODO: kind will eventually enable this by default and this patch will
# be unnecessary.
#
# See:
# https://github.com/kubernetes-sigs/kind/issues/2875
# https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration
# See: https://github.com/containerd/containerd/blob/main/docs/hosts.md
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
nodes:
- role: control-plane
image: kindest/node:v1.27.3
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- role: worker
image: kindest/node:v1.27.3
- role: worker
image: kindest/node:v1.27.3
EOF
echo "3. Add the registry config to the nodes"
# 3. Add the registry config to the nodes
#
# This is necessary because localhost resolves to loopback addresses that are
# network-namespace local.
# In other words: localhost in the container is not localhost on the host.
#
# We want a consistent name that works from both ends, so we tell containerd to
# alias localhost:${reg_port} to the registry container when pulling images
REGISTRY_DIR="/etc/containerd/certs.d/localhost:${reg_port}"
for node in $(kind get nodes); do
docker exec "${node}" mkdir -p "${REGISTRY_DIR}"
cat <<EOF | docker exec -i "${node}" cp /dev/stdin "${REGISTRY_DIR}/hosts.toml"
[host."http://${reg_name}:5000"]
EOF
done
echo "4. Connect the registry to the cluster network if not already connected"
# 4. Connect the registry to the cluster network if not already connected
# This allows kind to bootstrap the network but ensures they're on the same network
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then
docker network connect "kind" "${reg_name}"
fi
echo "5. Document the local registry"
# 5. Document the local registry
# https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: local-registry-hosting
namespace: kube-public
data:
localRegistryHosting.v1: |
host: "localhost:${reg_port}"
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF
echo "6. Install ingress-nginx"
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl -n ingress-nginx create secret tls mkcert --key /tmp/127.0.0.1.nip.io+1-key.pem --cert /tmp/127.0.0.1.nip.io+1.pem
kubectl -n ingress-nginx patch deployments.apps ingress-nginx-controller --type 'json' -p '[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--default-ssl-certificate=ingress-nginx/mkcert"}]'
curl https://raw.githubusercontent.com/numerique-gouv/tools/refs/heads/main/kind/create_cluster.sh | bash -s -- impress

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

View File

@@ -1,6 +1,13 @@
name: docs
services:
postgresql:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
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}
@@ -116,11 +139,15 @@ services:
volumes:
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
- keycloak
- app-dev
- y-provider
app-dev:
condition: service_started
y-provider:
condition: service_started
keycloak:
condition: service_healthy
restart: true
frontend-dev:
frontend:
user: "${DOCKER_USER:-1000}"
build:
context: .
@@ -128,16 +155,12 @@ services:
target: frontend-production
args:
API_ORIGIN: "http://localhost:8071"
Y_PROVIDER_URL: "ws://localhost:4444"
MEDIA_URL: "http://localhost:8083"
PUBLISH_AS_MIT: "false"
SW_DEACTIVATED: "true"
image: impress:frontend-development
ports:
- "3000:3000"
dockerize:
image: jwilder/dockerize
crowdin:
image: crowdin/cli:3.16.0
volumes:
@@ -151,7 +174,7 @@ services:
image: node:18
user: "${DOCKER_USER:-1000}"
environment:
HOME: /tmp
HOME: /tmp
volumes:
- ".:/app"
@@ -169,6 +192,11 @@ services:
kc_postgresql:
image: postgres:14.3
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 1s
timeout: 2s
retries: 300
ports:
- "5433:5432"
env_file:
@@ -187,6 +215,13 @@ services:
- --hostname-admin-url=http://localhost:8083/
- --hostname-strict=false
- --hostname-strict-https=false
- --health-enabled=true
- --metrics-enabled=true
healthcheck:
test: ["CMD", "curl", "--head", "-fsS", "http://localhost:8080/health/ready"]
interval: 1s
timeout: 2s
retries: 300
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
@@ -200,4 +235,6 @@ services:
ports:
- "8080:8080"
depends_on:
- kc_postgresql
kc_postgresql:
condition: service_healthy
restart: true

View File

@@ -4,54 +4,6 @@ server {
server_name localhost;
charset utf-8;
# Proxy auth for collaboration server
location /collaboration/ws/ {
# Collaboration Auth request configuration
auth_request /collaboration-auth;
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 Host $host;
}
location /collaboration-auth {
proxy_pass http://app-dev:8000/api/v1.0/documents/collaboration-auth/;
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
@@ -68,6 +20,8 @@ server {
# Get resource from Minio
proxy_pass http://minio:9000/impress-media-storage/;
proxy_set_header Host minio:9000;
add_header Content-Security-Policy "default-src 'none'" always;
}
location /media-auth {
@@ -88,5 +42,11 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Increase proxy buffer size to allow keycloak to send large
# header responses when a user is created.
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}

View File

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

19
docs/architecture.md Normal file
View File

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

BIN
docs/assets/banner-docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
docs/assets/docs-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

143
docs/env.md Normal file
View File

@@ -0,0 +1,143 @@
# Docs variables
Here we describe all environment variables that can be set for the docs application.
## impress-backend container
These are the environment variables you can set for the `impress-backend` container.
| Option | Description | default |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| DJANGO_ALLOWED_HOSTS | allowed hosts | [] |
| DJANGO_SECRET_KEY | secret key | |
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
| DB_ENGINE | engine to use for database connections | django.db.backends.postgresql_psycopg2 |
| DB_NAME | name of the database | impress |
| DB_USER | user to authenticate with | dinum |
| DB_PASSWORD | password to authenticate with | pass |
| DB_HOST | host of the database | localhost |
| DB_PORT | port of the database | 5432 |
| MEDIA_BASE_URL | | |
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
| AWS_S3_ACCESS_KEY_ID | access id for s3 endpoint | |
| AWS_S3_SECRET_ACCESS_KEY | access key for s3 endpoint | |
| AWS_S3_REGION_NAME | region name for s3 endpoint | |
| AWS_STORAGE_BUCKET_NAME | bucket name for s3 endpoint | impress-media-storage |
| DOCUMENT_IMAGE_MAX_SIZE | maximum size of document in bytes | 10485760 |
| LANGUAGE_CODE | default language | en-us |
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | throttle rate for api | 180/hour |
| API_USERS_LIST_THROTTLE_RATE_BURST | throttle rate for api on burst | 30/minute |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
| TRASHBIN_CUTOFF_DAYS | trashbin cutoff | 30 |
| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_BRAND_NAME | brand name for email | |
| DJANGO_EMAIL_HOST | host name of email | |
| DJANGO_EMAIL_HOST_USER | user to authenticate with on the email host | |
| DJANGO_EMAIL_HOST_PASSWORD | password to authenticate with on the email host | |
| DJANGO_EMAIL_LOGO_IMG | logo for the email | |
| DJANGO_EMAIL_PORT | port used to connect to email host | |
| DJANGO_EMAIL_USE_TLS | use tls for email host connection | false |
| DJANGO_EMAIL_USE_SSL | use sstl for email host connection | false |
| DJANGO_EMAIL_FROM | email address used as sender | from@example.com |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | true |
| DJANGO_CORS_ALLOWED_ORIGINS | list of origins allowed for CORS | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | list of origins allowed for CORS using regulair expressions | [] |
| SENTRY_DSN | sentry host | |
| COLLABORATION_API_URL | collaboration api host | |
| COLLABORATION_SERVER_SECRET | collaboration api secret | |
| COLLABORATION_WS_URL | collaboration websocket url | |
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
| FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
| FRONTEND_THEME | frontend theme to use | |
| POSTHOG_KEY | posthog key for analytics | |
| CRISP_WEBSITE_ID | crisp website id for support | |
| DJANGO_CELERY_BROKER_URL | celery broker url | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker transport options | {} |
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
| OIDC_CREATE_USER | create used on OIDC | false |
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
| OIDC_RP_CLIENT_ID | client id used for OIDC | impress |
| OIDC_RP_CLIENT_SECRET | client secret used for OIDC | |
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
| OIDC_RP_SCOPES | scopes requested for OIDC | openid email |
| LOGIN_REDIRECT_URL | login redirect url | |
| LOGIN_REDIRECT_URL_FAILURE | login redirect url on failure | |
| LOGOUT_REDIRECT_URL | logout redirect url | |
| OIDC_USE_NONCE | use nonce for OIDC | true |
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | faillback to email for identification | true |
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
| USER_OIDC_ESSENTIAL_CLAIMS | essential claims in OIDC token | [] |
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_MODEL | AI Model to use | |
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_FEATURE_ENABLED | Enable AI options | false |
| Y_PROVIDER_API_KEY | Y provider API key | |
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown |
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
| CONVERSION_API_SECURE | Require secure conversion api | false |
| LOGGING_LEVEL_LOGGERS_ROOT | default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| LOGGING_LEVEL_LOGGERS_APP | application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
| REDIS_URL | cache url | redis://redis:6379/1 |
| CACHES_DEFAULT_TIMEOUT | cache default timeout | 30 |
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
| THEME_CUSTOMIZATION_FILE_PATH | full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
## impress-frontend image
These are the environment variables you can set to build the `impress-frontend` image.
Depending on how you are building the front-end application, this variable is used in different ways.
If you want to build the Docker image, this variable is used as an argument in the build command.
Example:
```
docker build -f src/frontend/Dockerfile --target frontend-production --build-arg PUBLISH_AS_MIT=false docs-frontend:latest
```
If you want to build the front-end application using the yarn build command, you can edit the file `src/frontend/apps/impress/.env` with the `NODE_ENV=production` environment variable and modify it. Alternatively, you can use the listed environment variables with the prefix `NEXT_PUBLIC_` (for example, `NEXT_PUBLIC_PUBLISH_AS_MIT=false`).
Example:
```
cd src/frontend/apps/impress
NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
```
| Option | Description | default |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| API_ORIGIN | backend domain - it uses the current domain if not initialized | |
| SW_DEACTIVATED | To not install the service worker | |
| PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true |
Packages with licences incompatible with the MIT licence:
* `xl-docx-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE)
In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features.
⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your [BlockNote licensing](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE) or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.

View File

@@ -0,0 +1,163 @@
image:
repository: lasuite/impress-backend
pullPolicy: Always
tag: "latest"
backend:
replicas: 1
envVars:
COLLABORATION_API_URL: https://impress.127.0.0.1.nip.io/collaboration/api/
COLLABORATION_SERVER_SECRET: my-secret
DJANGO_CSRF_TRUSTED_ORIGINS: https://impress.127.0.0.1.nip.io
DJANGO_CONFIGURATION: Feature
DJANGO_ALLOWED_HOSTS: impress.127.0.0.1.nip.io
DJANGO_SERVER_TO_SERVER_API_TOKENS: secret-api-key
DJANGO_SECRET_KEY: AgoodOrAbadKey
DJANGO_SETTINGS_MODULE: impress.settings
DJANGO_SUPERUSER_PASSWORD: admin
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
DJANGO_EMAIL_USE_SSL: False
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
LOGGING_LEVEL_LOGGERS_ROOT: INFO
LOGGING_LEVEL_LOGGERS_APP: INFO
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/session/end
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
OIDC_VERIFY_SSL: False
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
OIDC_REDIRECT_ALLOWED_HOSTS: https://impress.127.0.0.1.nip.io
OIDC_AUTH_REQUEST_EXTRA_PARAMS: "{'acr_values': 'eidas1'}"
LOGIN_REDIRECT_URL: https://impress.127.0.0.1.nip.io
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
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: root
AWS_S3_SECRET_ACCESS_KEY: password
AWS_STORAGE_BUCKET_NAME: impress-media-storage
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
Y_PROVIDER_API_BASE_URL: http://impress-y-provider:443/api/
Y_PROVIDER_API_KEY: my-secret
migrate:
command:
- "/bin/sh"
- "-c"
- |
python manage.py migrate --no-input &&
python manage.py create_demo --force
restartPolicy: Never
command:
- "gunicorn"
- "-c"
- "/usr/local/etc/gunicorn/impress.py"
- "impress.wsgi:application"
- "--reload"
createsuperuser:
command:
- "/bin/sh"
- "-c"
- |
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumes:
- name: certs
configMap:
name: certifi
items:
- key: cacert.pem
path: cacert.pem
frontend:
envVars:
PORT: 8080
NEXT_PUBLIC_API_ORIGIN: https://impress.127.0.0.1.nip.io
replicas: 1
image:
repository: lasuite/impress-frontend
pullPolicy: Always
tag: "latest"
yProvider:
replicas: 1
image:
repository: lasuite/impress-y-provider
pullPolicy: Always
tag: "latest"
envVars:
COLLABORATION_LOGGING: true
COLLABORATION_SERVER_ORIGIN: https://impress.127.0.0.1.nip.io
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
ingressCollaborationWS:
enabled: true
host: impress.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/collaboration-auth/
ingressCollaborationApi:
enabled: true
host: impress.127.0.0.1.nip.io
ingressAdmin:
enabled: true
host: impress.127.0.0.1.nip.io
ingressMedia:
enabled: true
host: impress.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/auth-url: https://impress.127.0.0.1.nip.io/api/v1.0/documents/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: minio.impress.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /impress-media-storage/$1
serviceMedia:
host: minio.impress.svc.cluster.local
port: 9000

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
auth:
rootUser: root
rootPassword: password
provisioning:
enabled: true
buckets:
- name: impress-media-storage
versioning: true

View File

@@ -0,0 +1,7 @@
auth:
username: dinum
password: pass
database: impress
tls:
enabled: true
autoGenerated: true

View File

@@ -0,0 +1,4 @@
auth:
password: pass
architecture: standalone

230
docs/installation.md Normal file
View File

@@ -0,0 +1,230 @@
# Installation on a k8s cluster
This document is a step-by-step guide that describes how to install Docs on a k8s cluster without AI features. It's a teaching document to learn how it works. It needs to be adapted for a production environment.
## Prerequisites
- k8s cluster with an nginx-ingress controller
- an OIDC provider (if you don't have one, we provide an example)
- a PostgreSQL server (if you don't have one, we provide an example)
- a Memcached server (if you don't have one, we provide an example)
- a S3 bucket (if you don't have one, we provide an example)
### Test cluster
If you do not have a test cluster, you can install everything on a local Kind cluster. In this case, the simplest way is to use our script **bin/start-kind.sh**.
To be able to use the script, you need to install:
- Docker (https://docs.docker.com/desktop/)
- Kind (https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
- Mkcert (https://github.com/FiloSottile/mkcert#installation)
- Helm (https://helm.sh/docs/intro/quickstart/#install-helm)
```
./bin/start-kind.sh
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4700 100 4700 0 0 92867 0 --:--:-- --:--:-- --:--:-- 94000
0. Create ca
The local CA is already installed in the system trust store! 👍
The local CA is already installed in the Firefox and/or Chrome/Chromium trust store! 👍
Created a new certificate valid for the following names 📜
- "127.0.0.1.nip.io"
- "*.127.0.0.1.nip.io"
Reminder: X.509 wildcards only go one level deep, so this won't match a.b.127.0.0.1.nip.io
The certificate is at "./127.0.0.1.nip.io+1.pem" and the key at "./127.0.0.1.nip.io+1-key.pem" ✅
It will expire on 24 March 2027 🗓
1. Create registry container unless it already exists
2. Create kind cluster with containerd registry config dir enabled
Creating cluster "suite" ...
✓ Ensuring node image (kindest/node:v1.27.3) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
Set kubectl context to "kind-suite"
You can now use your cluster with:
kubectl cluster-info --context kind-suite
Thanks for using kind! 😊
3. Add the registry config to the nodes
4. Connect the registry to the cluster network if not already connected
5. Document the local registry
configmap/local-registry-hosting created
Warning: resource configmaps/coredns is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
configmap/coredns configured
deployment.apps/coredns restarted
6. Install ingress-nginx
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
secret/mkcert created
deployment.apps/ingress-nginx-controller patched
7. Setup namespace
namespace/impress created
Context "kind-suite" modified.
secret/mkcert created
$ kubectl -n ingress-nginx get po
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-t55ph 0/1 Completed 0 2m56s
ingress-nginx-admission-patch-94dvt 0/1 Completed 1 2m56s
ingress-nginx-controller-57c548c4cd-2rx47 1/1 Running 0 2m56s
```
When your k8s cluster is ready (the ingress nginx controller is up), you can start the deployment. This cluster is special because it uses the `*.127.0.0.1.nip.io` domain and mkcert certificates to have full HTTPS support and easy domain name management.
Please remember that `*.127.0.0.1.nip.io` will always resolve to `127.0.0.1`, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
## Preparation
### What do you use to authenticate your users?
Docs uses OIDC, so if you already have an OIDC provider, obtain the necessary information to use it. In the next step, we will see how to configure Django (and thus Docs) to use it. If you do not have a provider, we will show you how to deploy a local Keycloak instance (this is not a production deployment, just a demo).
```
$ kubectl create namespace impress
$ kubectl config set-context --current --namespace=impress
$ helm install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak -f examples/keycloak.values.yaml
$ #wait until
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 6m48s
keycloak-postgresql-0 1/1 Running 0 6m48s
```
From here the important information you will need are:
```yaml
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/session/end
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email"
```
You can find these values in **examples/keycloak.values.yaml**
### Find redis server connection values
Docs needs a redis so we start by deploying one:
```
$ helm install redis oci://registry-1.docker.io/bitnamicharts/redis -f examples/redis.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 26m
keycloak-postgresql-0 1/1 Running 0 26m
redis-master-0 1/1 Running 0 35s
```
### Find postgresql connection values
Docs uses a postgresql database as backend, so if you have a provider, obtain the necessary information to use it. If you don't, you can install a postgresql testing environment as follow:
```
$ helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql -f examples/postgresql.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 28m
keycloak-postgresql-0 1/1 Running 0 28m
postgresql-0 1/1 Running 0 14m
redis-master-0 1/1 Running 0 42s
```
From here the important information you will need are:
```yaml
DB_HOST: postgres-postgresql
DB_NAME: impress
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
### Find s3 bucket connection values
Docs uses an s3 bucket to store documents, so if you have a provider obtain the necessary information to use it. If you don't, you can install a local minio testing environment as follow:
```
$ helm install minio oci://registry-1.docker.io/bitnamicharts/minio -f examples/minio.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
keycloak-0 1/1 Running 0 38m
keycloak-postgresql-0 1/1 Running 0 38m
minio-84f5c66895-bbhsk 1/1 Running 0 42s
minio-provisioning-2b5sq 0/1 Completed 0 42s
postgresql-0 1/1 Running 0 24m
redis-master-0 1/1 Running 0 10m
```
## Deployment
Now you are ready to deploy Docs without AI. AI requires more dependencies (OpenAI API). To deploy Docs you need to provide all previous information to the helm chart.
```
$ helm repo add impress https://suitenumerique.github.io/docs/
$ helm repo update
$ helm install impress impress/docs -f examples/impress.values.yaml
$ kubectl get po
NAME READY STATUS RESTARTS AGE
impress-docs-backend-96558758d-xtkbp 0/1 Running 0 79s
impress-docs-backend-createsuperuser-r7ltc 0/1 Completed 0 79s
impress-docs-backend-migrate-c949s 0/1 Completed 0 79s
impress-docs-frontend-6749f644f7-p5s42 1/1 Running 0 79s
impress-docs-y-provider-6947fd8f54-78f2l 1/1 Running 0 79s
keycloak-0 1/1 Running 0 48m
keycloak-postgresql-0 1/1 Running 0 48m
minio-84f5c66895-bbhsk 1/1 Running 0 10m
minio-provisioning-2b5sq 0/1 Completed 0 10m
postgresql-0 1/1 Running 0 34m
redis-master-0 1/1 Running 0 20m
```
## Test your deployment
In order to test your deployment you have to log into your instance. If you exclusively use our examples you can do:
```
$ kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
impress-docs <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-admin <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-collaboration-api <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-media <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
impress-docs-ws <none> impress.127.0.0.1.nip.io localhost 80, 443 114s
keycloak <none> keycloak.127.0.0.1.nip.io localhost 80 49m
```
You can use Docs at https://impress.127.0.0.1.nip.io. The provisionning user in keycloak is impress/impress.

56
docs/theming.md Normal file
View File

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

View File

@@ -50,15 +50,14 @@ OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# AI
AI_FEATURE_ENABLED=true
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama
# Collaboration
COLLABORATION_API_URL=http://nginx:8083/collaboration/api/
COLLABORATION_API_URL=http://y-provider:4444/collaboration/api/
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
COLLABORATION_SERVER_SECRET=my-secret
COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws/
# Frontend
FRONTEND_THEME=dsfr
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ class GitmojiTitle(LineRule):
"https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json"
).json()["gitmojis"]
emojis = [item["emoji"] for item in gitmojis]
pattern = r"^({:s})\(.*\)\s[a-z].*$".format("|".join(emojis))
pattern = r"^({:s})\(.*\)\s[a-zA-Z].*$".format("|".join(emojis))
if not re.search(pattern, title):
violation_msg = 'Title does not match regex "<gitmoji>(<scope>) <subject>"'
return [RuleViolation(self.id, violation_msg, title)]

27
publiccode.yml Normal file
View File

@@ -0,0 +1,27 @@
publiccodeYmlVersion: "2.4.0"
name: Docs
url: https://github.com/suitenumerique/docs
landingURL: https://github.com/suitenumerique/docs
creationDate: 2023-12-10
logo: https://raw.githubusercontent.com/suitenumerique/docs/main/docs/assets/docs-logo.png
usedBy:
- Direction interministériel du numérique (DINUM)
fundedBy:
- name: Direction interministériel du numérique (DINUM)
url: https://www.numerique.gouv.fr
roadmap: "https://github.com/orgs/suitenumerique/projects/2/views/1"
softwareType: "standalone/other"
description:
en:
shortDescription: "The open source document editor where your notes can become knowledge through live collaboration"
fr:
shortDescription: "L'éditeur de documents open source où vos notes peuvent devenir des connaissances grâce à la collaboration en direct."
legal:
license: MIT
maintenance:
type: internal
contacts:
- name: "Virgile Deville"
email: "virgile.deville@numerique.gouv.fr"
- name: "samuel.paccoud"
email: "samuel.paccoud@numerique.gouv.fr"

View File

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

Submodule secrets deleted from 38594182e8

0
secu-audit.md Normal file
View File

View File

@@ -4,12 +4,16 @@ from django.contrib import admin
from django.contrib.auth import admin as auth_admin
from django.utils.translation import gettext_lazy as _
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from . import models
class TemplateAccessInline(admin.TabularInline):
"""Inline admin class for template accesses."""
autocomplete_fields = ["user"]
model = models.TemplateAccess
extra = 0
@@ -111,14 +115,49 @@ class TemplateAdmin(admin.ModelAdmin):
class DocumentAccessInline(admin.TabularInline):
"""Inline admin class for template accesses."""
autocomplete_fields = ["user"]
model = models.DocumentAccess
extra = 0
@admin.register(models.Document)
class DocumentAdmin(admin.ModelAdmin):
class DocumentAdmin(TreeAdmin):
"""Document admin interface declaration."""
fieldsets = (
(
None,
{
"fields": (
"id",
"title",
)
},
),
(
_("Permissions"),
{
"fields": (
"creator",
"link_reach",
"link_role",
)
},
),
(
_("Tree structure"),
{
"fields": (
"path",
"depth",
"numchild",
"duplicated_from",
"attachments",
)
},
),
)
form = movenodeform_factory(models.Document)
inlines = (DocumentAccessInline,)
list_display = (
"id",
@@ -128,6 +167,16 @@ class DocumentAdmin(admin.ModelAdmin):
"created_at",
"updated_at",
)
readonly_fields = (
"attachments",
"creator",
"depth",
"duplicated_from",
"id",
"numchild",
"path",
)
search_fields = ("id", "title")
@admin.register(models.Invitation)

View File

@@ -17,9 +17,10 @@ def exception_handler(exc, context):
https://gist.github.com/twidi/9d55486c36b6a51bdcb05ce3a763e79f
"""
if isinstance(exc, ValidationError):
detail = exc.message_dict
if hasattr(exc, "message"):
detail = None
if hasattr(exc, "message_dict"):
detail = exc.message_dict
elif hasattr(exc, "message"):
detail = exc.message
elif hasattr(exc, "messages"):
detail = exc.messages

View File

@@ -1,5 +1,7 @@
"""API filters for Impress' core application."""
import unicodedata
from django.utils.translation import gettext_lazy as _
import django_filters
@@ -7,7 +9,50 @@ import django_filters
from core import models
def remove_accents(value):
"""Remove accents from a string (vélo -> velo)."""
return "".join(
c
for c in unicodedata.normalize("NFD", value)
if unicodedata.category(c) != "Mn"
)
class AccentInsensitiveCharFilter(django_filters.CharFilter):
"""
A custom CharFilter that filters on the accent-insensitive value searched.
"""
def filter(self, qs, value):
"""
Apply the filter to the queryset using the unaccented version of the field.
Args:
qs: The queryset to filter.
value: The value to search for in the unaccented field.
Returns:
A filtered queryset.
"""
if value:
value = remove_accents(value)
return super().filter(qs, value)
class DocumentFilter(django_filters.FilterSet):
"""
Custom filter for filtering documents on title (accent and case insensitive).
"""
title = AccentInsensitiveCharFilter(
field_name="title", lookup_expr="unaccent__icontains", label=_("Title")
)
class Meta:
model = models.Document
fields = ["title"]
class ListDocumentFilter(DocumentFilter):
"""
Custom filter for filtering documents.
"""
@@ -18,13 +63,10 @@ class DocumentFilter(django_filters.FilterSet):
is_favorite = django_filters.BooleanFilter(
method="filter_is_favorite", label=_("Favorite")
)
title = django_filters.CharFilter(
field_name="title", lookup_expr="icontains", label=_("Title")
)
class Meta:
model = models.Document
fields = ["is_creator_me", "is_favorite", "link_reach", "title"]
fields = ["is_creator_me", "is_favorite", "title"]
# pylint: disable=unused-argument
def filter_is_creator_me(self, queryset, name, value):
@@ -63,7 +105,4 @@ class DocumentFilter(django_filters.FilterSet):
if not user.is_authenticated:
return queryset
if value:
return queryset.filter(favorited_by_users__user=user)
return queryset.exclude(favorited_by_users__user=user)
return queryset.filter(is_favorite=bool(value))

View File

@@ -2,13 +2,15 @@
from django.core import exceptions
from django.db.models import Q
from django.http import Http404
from rest_framework import permissions
from core.models import DocumentAccess, RoleChoices
from core.models import DocumentAccess, RoleChoices, get_trashbin_cutoff
ACTION_FOR_METHOD_TO_PERMISSION = {
"versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"}
"versions_detail": {"DELETE": "versions_destroy", "GET": "versions_retrieve"},
"children": {"GET": "children_list", "POST": "children_create"},
}
@@ -109,3 +111,26 @@ class AccessPermission(permissions.BasePermission):
except KeyError:
pass
return abilities.get(action, False)
class DocumentAccessPermission(AccessPermission):
"""Subclass to handle soft deletion specificities."""
def has_object_permission(self, request, view, obj):
"""
Return a 404 on deleted documents
- for which the trashbin cutoff is past
- for which the current user is not owner of the document or one of its ancestors
"""
if (
deleted_at := obj.ancestors_deleted_at
) and deleted_at < get_trashbin_cutoff():
raise Http404
# Compute permission first to ensure the "user_roles" attribute is set
has_permission = super().has_object_permission(request, view, obj)
if obj.ancestors_deleted_at and not RoleChoices.OWNER in obj.user_roles:
raise Http404
return has_permission

View File

@@ -1,6 +1,8 @@
"""Client serializers for the impress core app."""
import binascii
import mimetypes
from base64 import b64decode
from django.conf import settings
from django.db.models import Q
@@ -10,7 +12,7 @@ from django.utils.translation import gettext_lazy as _
import magic
from rest_framework import exceptions, serializers
from core import enums, models
from core import enums, models, utils
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
ConversionError,
@@ -21,6 +23,26 @@ from core.services.converter_services import (
class UserSerializer(serializers.ModelSerializer):
"""Serialize users."""
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name", "language"]
read_only_fields = ["id", "email", "full_name", "short_name"]
class UserLightSerializer(UserSerializer):
"""Serialize users with limited fields."""
id = serializers.SerializerMethodField(read_only=True)
email = serializers.SerializerMethodField(read_only=True)
def get_id(self, _user):
"""Return always None. Here to have the same fields than in UserSerializer."""
return None
def get_email(self, _user):
"""Return always None. Here to have the same fields than in UserSerializer."""
return None
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name"]
@@ -118,6 +140,17 @@ class DocumentAccessSerializer(BaseAccessSerializer):
read_only_fields = ["id", "abilities"]
class DocumentAccessLightSerializer(DocumentAccessSerializer):
"""Serialize document accesses with limited fields."""
user = UserLightSerializer(read_only=True)
class Meta:
model = models.DocumentAccess
fields = ["id", "user", "team", "role", "abilities"]
read_only_fields = ["id", "team", "role", "abilities"]
class TemplateAccessSerializer(BaseAccessSerializer):
"""Serialize template accesses."""
@@ -128,53 +161,79 @@ class TemplateAccessSerializer(BaseAccessSerializer):
read_only_fields = ["id", "abilities"]
class BaseResourceSerializer(serializers.ModelSerializer):
"""Serialize documents."""
abilities = serializers.SerializerMethodField(read_only=True)
accesses = TemplateAccessSerializer(many=True, read_only=True)
def get_abilities(self, document) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
return document.get_abilities(request.user)
return {}
class ListDocumentSerializer(BaseResourceSerializer):
class ListDocumentSerializer(serializers.ModelSerializer):
"""Serialize documents with limited fields for display in lists."""
is_favorite = serializers.BooleanField(read_only=True)
nb_accesses = serializers.IntegerField(read_only=True)
nb_accesses_ancestors = serializers.IntegerField(read_only=True)
nb_accesses_direct = serializers.IntegerField(read_only=True)
user_roles = serializers.SerializerMethodField(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.Document
fields = [
"id",
"abilities",
"content",
"created_at",
"creator",
"depth",
"excerpt",
"is_favorite",
"link_role",
"link_reach",
"nb_accesses",
"nb_accesses_ancestors",
"nb_accesses_direct",
"numchild",
"path",
"title",
"updated_at",
"user_roles",
]
read_only_fields = [
"id",
"abilities",
"created_at",
"creator",
"depth",
"excerpt",
"is_favorite",
"link_role",
"link_reach",
"nb_accesses",
"nb_accesses_ancestors",
"nb_accesses_direct",
"numchild",
"path",
"updated_at",
"user_roles",
]
def get_abilities(self, document) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
paths_links_mapping = self.context.get("paths_links_mapping", None)
# Retrieve ancestor links from paths_links_mapping (if provided)
ancestors_links = (
paths_links_mapping.get(document.path[: -document.steplen])
if paths_links_mapping
else None
)
return document.get_abilities(request.user, ancestors_links=ancestors_links)
return {}
def get_user_roles(self, document):
"""
Return roles of the logged-in user for the current document,
taking into account ancestors.
"""
request = self.context.get("request")
if request:
return document.get_roles(request.user)
return []
class DocumentSerializer(ListDocumentSerializer):
"""Serialize documents with all fields for display in detail views."""
@@ -189,23 +248,34 @@ class DocumentSerializer(ListDocumentSerializer):
"content",
"created_at",
"creator",
"depth",
"excerpt",
"is_favorite",
"link_role",
"link_reach",
"nb_accesses",
"nb_accesses_ancestors",
"nb_accesses_direct",
"numchild",
"path",
"title",
"updated_at",
"user_roles",
]
read_only_fields = [
"id",
"abilities",
"created_at",
"creator",
"is_avorite",
"depth",
"is_favorite",
"link_role",
"link_reach",
"nb_accesses",
"nb_accesses_ancestors",
"nb_accesses_direct",
"numchild",
"path",
"updated_at",
"user_roles",
]
def get_fields(self):
@@ -231,6 +301,65 @@ class DocumentSerializer(ListDocumentSerializer):
return value
def validate_content(self, value):
"""Validate the content field."""
if not value:
return None
try:
b64decode(value, validate=True)
except binascii.Error as err:
raise serializers.ValidationError("Invalid base64 content.") from err
return value
def save(self, **kwargs):
"""
Process the content field to extract attachment keys and update the document's
"attachments" field for access control.
"""
content = self.validated_data.get("content", "")
extracted_attachments = set(utils.extract_attachments(content))
existing_attachments = (
set(self.instance.attachments or []) if self.instance else set()
)
new_attachments = extracted_attachments - existing_attachments
if new_attachments:
attachments_documents = (
models.Document.objects.filter(
attachments__overlap=list(new_attachments)
)
.only("path", "attachments")
.order_by("path")
)
user = self.context["request"].user
readable_per_se_paths = (
models.Document.objects.readable_per_se(user)
.order_by("path")
.values_list("path", flat=True)
)
readable_attachments_paths = utils.filter_descendants(
[doc.path for doc in attachments_documents],
readable_per_se_paths,
skip_sorting=True,
)
readable_attachments = set()
for document in attachments_documents:
if document.path not in readable_attachments_paths:
continue
readable_attachments.update(set(document.attachments) & new_attachments)
# Update attachments with readable keys
self.validated_data["attachments"] = list(
existing_attachments | readable_attachments
)
return super().save(**kwargs)
class ServerCreateDocumentSerializer(serializers.Serializer):
"""
@@ -264,13 +393,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,9 +412,11 @@ 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(
document = models.Document.add_root(
title=validated_data["title"],
content=document_content,
creator=user,
@@ -302,7 +437,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 +452,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.
@@ -322,7 +459,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
raise NotImplementedError("Update is not supported for this serializer.")
class LinkDocumentSerializer(BaseResourceSerializer):
class LinkDocumentSerializer(serializers.ModelSerializer):
"""
Serialize link configuration for documents.
We expose it separately from document in order to simplify and secure access control.
@@ -336,6 +473,27 @@ class LinkDocumentSerializer(BaseResourceSerializer):
]
class DocumentDuplicationSerializer(serializers.Serializer):
"""
Serializer for duplicating a document.
Allows specifying whether to keep access permissions.
"""
with_accesses = serializers.BooleanField(default=False)
def create(self, validated_data):
"""
This serializer is not intended to create objects.
"""
raise NotImplementedError("This serializer does not support creation.")
def update(self, instance, validated_data):
"""
This serializer is not intended to update objects.
"""
raise NotImplementedError("This serializer does not support updating.")
# Suppress the warning about not implementing `create` and `update` methods
# since we don't use a model and only rely on the serializer for validation
# pylint: disable=abstract-method
@@ -380,6 +538,8 @@ class FileUploadSerializer(serializers.Serializer):
raise serializers.ValidationError("Could not determine file extension.")
self.context["expected_extension"] = extension
self.context["content_type"] = magic_mime_type
self.context["file_name"] = file.name
return file
@@ -387,12 +547,17 @@ 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"]
attrs["file_name"] = self.context["file_name"]
return attrs
class TemplateSerializer(BaseResourceSerializer):
class TemplateSerializer(serializers.ModelSerializer):
"""Serialize templates."""
abilities = serializers.SerializerMethodField(read_only=True)
accesses = TemplateAccessSerializer(many=True, read_only=True)
class Meta:
model = models.Template
fields = [
@@ -406,6 +571,13 @@ class TemplateSerializer(BaseResourceSerializer):
]
read_only_fields = ["id", "accesses", "abilities"]
def get_abilities(self, document) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
if request:
return document.get_abilities(request.user)
return {}
# pylint: disable=abstract-method
class DocumentGenerationSerializer(serializers.Serializer):
@@ -529,3 +701,37 @@ class AITranslateSerializer(serializers.Serializer):
if len(value.strip()) == 0:
raise serializers.ValidationError("Text field cannot be empty.")
return value
class MoveDocumentSerializer(serializers.Serializer):
"""
Serializer for validating input data to move a document within the tree structure.
Fields:
- target_document_id (UUIDField): The ID of the target parent document where the
document should be moved. This field is required and must be a valid UUID.
- position (ChoiceField): Specifies the position of the document in relation to
the target parent's children.
Choices:
- "first-child": Place the document as the first child of the target parent.
- "last-child": Place the document as the last child of the target parent (default).
- "left": Place the document as the left sibling of the target parent.
- "right": Place the document as the right sibling of the target parent.
Example:
Input payload for moving a document:
{
"target_document_id": "123e4567-e89b-12d3-a456-426614174000",
"position": "first-child"
}
Notes:
- The `target_document_id` is mandatory.
- The `position` defaults to "last-child" if not provided.
"""
target_document_id = serializers.UUIDField(required=True)
position = serializers.ChoiceField(
choices=enums.MoveNodePositionChoices.choices,
default=enums.MoveNodePositionChoices.LAST_CHILD,
)

View File

@@ -11,6 +11,58 @@ import botocore
from rest_framework.throttling import BaseThrottle
def nest_tree(flat_list, steplen):
"""
Convert a flat list of serialized documents into a nested tree making advantage
of the`path` field and its step length.
"""
node_dict = {}
roots = []
# Sort the flat list by path to ensure parent nodes are processed first
flat_list.sort(key=lambda x: x["path"])
for node in flat_list:
node["children"] = [] # Initialize children list
node_dict[node["path"]] = node
# Determine parent path
parent_path = node["path"][:-steplen]
if parent_path in node_dict:
node_dict[parent_path]["children"].append(node)
else:
roots.append(node) # Collect root nodes
if len(roots) > 1:
raise ValueError("More than one root element detected.")
return roots[0] if roots else None
def filter_root_paths(paths, skip_sorting=False):
"""
Filters root paths from a list of paths representing a tree structure.
A root path is defined as a path that is not a prefix of any other path.
Args:
paths (list of str): The list of paths.
Returns:
list of str: The filtered list of root paths.
"""
if not skip_sorting:
paths.sort()
root_paths = []
for path in paths:
# If the current path is not a prefix of the last added root path, add it
if not root_paths or not path.startswith(root_paths[-1]):
root_paths.append(path)
return root_paths
def generate_s3_authorization_headers(key):
"""
Generate authorization headers for an s3 object.

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,15 @@ from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
class AuthenticatedServer:
"""
Simple class to represent an authenticated server to be used along the
IsAuthenticated permission.
"""
is_authenticated = True
class ServerToServerAuthentication(BaseAuthentication):
"""
Custom authentication class for server-to-server requests.
@@ -39,13 +48,16 @@ class ServerToServerAuthentication(BaseAuthentication):
# Validate token format and existence
auth_parts = auth_header.split(" ")
if len(auth_parts) != 2 or auth_parts[0] != self.TOKEN_TYPE:
raise AuthenticationFailed("Invalid authorization header.")
# Do not raise here to leave the door open for other authentication methods
return None
token = auth_parts[1]
if token not in settings.SERVER_TO_SERVER_API_TOKENS:
raise AuthenticationFailed("Invalid server-to-server token.")
# Do not raise here to leave the door open for other authentication methods
return None
# Authentication is successful, but no user is authenticated
# Authentication is successful
return AuthenticatedServer(), token
def authenticate_header(self, request):
"""Return the WWW-Authenticate header value."""

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,47 @@
Core application enums declaration
"""
from django.conf import global_settings
import re
from enum import StrEnum
from django.conf import global_settings, settings
from django.db import models
from django.utils.translation import gettext_lazy as _
ATTACHMENTS_FOLDER = "attachments"
UUID_REGEX = (
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
)
FILE_EXT_REGEX = r"\.[a-zA-Z0-9]{1,10}"
MEDIA_STORAGE_URL_PATTERN = re.compile(
f"{settings.MEDIA_URL:s}(?P<pk>{UUID_REGEX:s})/"
f"(?P<attachment>{ATTACHMENTS_FOLDER:s}/{UUID_REGEX:s}(?:-unsafe)?{FILE_EXT_REGEX:s})$"
)
MEDIA_STORAGE_URL_EXTRACT = re.compile(
f"{settings.MEDIA_URL:s}({UUID_REGEX}/{ATTACHMENTS_FOLDER}/{UUID_REGEX}{FILE_EXT_REGEX})"
)
# In Django's code base, `LANGUAGES` is set by default with all supported languages.
# We can use it for the choice of languages which should not be limited to the few languages
# active in the app.
# pylint: disable=no-member
ALL_LANGUAGES = {language: _(name) for language, name in global_settings.LANGUAGES}
class MoveNodePositionChoices(models.TextChoices):
"""Defines the possible positions when moving a django-treebeard node."""
FIRST_CHILD = "first-child", _("First child")
LAST_CHILD = "last-child", _("Last child")
FIRST_SIBLING = "first-sibling", _("First sibling")
LAST_SIBLING = "last-sibling", _("Last sibling")
LEFT = "left", _("Left")
RIGHT = "right", _("Right")
class DocumentAttachmentStatus(StrEnum):
"""Defines the possible statuses for an attachment."""
PROCESSING = "processing"
READY = "ready"

View File

@@ -13,6 +13,22 @@ from core import models
fake = Faker()
YDOC_HELLO_WORLD_BASE64 = (
"AR717vLVDgAHAQ5kb2N1bWVudC1zdG9yZQMKYmxvY2tHcm91cAcA9e7y1Q4AAw5ibG9ja0NvbnRh"
"aW5lcgcA9e7y1Q4BAwdoZWFkaW5nBwD17vLVDgIGBgD17vLVDgMGaXRhbGljAnt9hPXu8tUOBAVI"
"ZWxsb4b17vLVDgkGaXRhbGljBG51bGwoAPXu8tUOAg10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y"
"1Q4CBWxldmVsAX0BKAD17vLVDgECaWQBdyQwNGQ2MjM0MS04MzI2LTQyMzYtYTA4My00ODdlMjZm"
"YWQyMzAoAPXu8tUOAQl0ZXh0Q29sb3IBdwdkZWZhdWx0KAD17vLVDgEPYmFja2dyb3VuZENvbG9y"
"AXcHZGVmYXVsdIf17vLVDgEDDmJsb2NrQ29udGFpbmVyBwD17vLVDhADDmJ1bGxldExpc3RJdGVt"
"BwD17vLVDhEGBAD17vLVDhIBd4b17vLVDhMEYm9sZAJ7fYT17vLVDhQCb3KG9e7y1Q4WBGJvbGQE"
"bnVsbIT17vLVDhcCbGQoAPXu8tUOEQ10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y1Q4QAmlkAXck"
"ZDM1MWUwNjgtM2U1NS00MjI2LThlYTUtYWJiMjYzMTk4ZTJhKAD17vLVDhAJdGV4dENvbG9yAXcH"
"ZGVmYXVsdCgA9e7y1Q4QD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHSH9e7y1Q4QAw5ibG9ja0Nv"
"bnRhaW5lcgcA9e7y1Q4eAwlwYXJhZ3JhcGgoAPXu8tUOHw10ZXh0QWxpZ25tZW50AXcEbGVmdCgA"
"9e7y1Q4eAmlkAXckODk3MDBjMDctZTBlMS00ZmUwLWFjYTItODQ5MzIwOWE3ZTQyKAD17vLVDh4J"
"dGV4dENvbG9yAXcHZGVmYXVsdCgA9e7y1Q4eD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHQA"
)
class UserFactory(factory.django.DjangoModelFactory):
"""A factory to random users for testing purposes."""
@@ -46,6 +62,23 @@ class UserFactory(factory.django.DjangoModelFactory):
UserTemplateAccessFactory(user=self, role="owner")
class ParentNodeFactory(factory.declarations.ParameteredAttribute):
"""Custom factory attribute for setting the parent node."""
def generate(self, step, params):
"""
Generate a parent node for the factory.
This method is invoked during the factory's build process to determine the parent
node of the current object being created. If `params` is provided, it uses the factory's
metadata to recursively create or fetch the parent node. Otherwise, it returns `None`.
"""
if not params:
return None
subfactory = step.builder.factory_meta.factory
return step.recurse(subfactory, params)
class DocumentFactory(factory.django.DjangoModelFactory):
"""A factory to create documents"""
@@ -54,9 +87,13 @@ class DocumentFactory(factory.django.DjangoModelFactory):
django_get_or_create = ("title",)
skip_postgeneration_save = True
parent = ParentNodeFactory()
title = factory.Sequence(lambda n: f"document{n}")
content = factory.Sequence(lambda n: f"content{n}")
excerpt = factory.Sequence(lambda n: f"excerpt{n}")
content = YDOC_HELLO_WORLD_BASE64
creator = factory.SubFactory(UserFactory)
deleted_at = None
link_reach = factory.fuzzy.FuzzyChoice(
[a[0] for a in models.LinkReachChoices.choices]
)
@@ -64,6 +101,29 @@ class DocumentFactory(factory.django.DjangoModelFactory):
[r[0] for r in models.LinkRoleChoices.choices]
)
@classmethod
def _create(cls, model_class, *args, **kwargs):
"""
Custom creation logic for the factory: creates a document as a child node if
a parent is provided; otherwise, creates it as a root node.
"""
parent = kwargs.pop("parent", None)
if parent:
# Add as a child node
kwargs["ancestors_deleted_at"] = (
kwargs.get("ancestors_deleted_at") or parent.ancestors_deleted_at
)
return parent.add_child(instance=model_class(**kwargs))
# Add as a root node
return model_class.add_root(instance=model_class(**kwargs))
@factory.lazy_attribute
def ancestors_deleted_at(self):
"""Should always be set when "deleted_at" is set."""
return self.deleted_at
@factory.post_generation
def users(self, create, extracted, **kwargs):
"""Add users to document from a given list of users with or without roles."""
@@ -74,6 +134,16 @@ class DocumentFactory(factory.django.DjangoModelFactory):
else:
UserDocumentAccessFactory(document=self, user=item[0], role=item[1])
@factory.post_generation
def teams(self, create, extracted, **kwargs):
"""Add teams to document from a given list of teams with or without roles."""
if create and extracted:
for item in extracted:
if isinstance(item, str):
TeamDocumentAccessFactory(document=self, team=item)
else:
TeamDocumentAccessFactory(document=self, team=item[0], role=item[1])
@factory.post_generation
def link_traces(self, create, extracted, **kwargs):
"""Add link traces to document from a given list of users."""

View File

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

View File

View File

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

View File

@@ -1,166 +1,552 @@
# Generated by Django 5.0.3 on 2024-05-28 20:29
import uuid
import django.contrib.auth.models
import django.core.validators
import django.db.models.deletion
import timezone_field.fields
import uuid
from django.conf import settings
from django.db import migrations, models
import timezone_field.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name='Document',
name="Document",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('title', models.CharField(max_length=255, verbose_name='title')),
('is_public', models.BooleanField(default=False, help_text='Whether this document is public for anyone to use.', verbose_name='public')),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("title", models.CharField(max_length=255, verbose_name="title")),
(
"is_public",
models.BooleanField(
default=False,
help_text="Whether this document is public for anyone to use.",
verbose_name="public",
),
),
],
options={
'verbose_name': 'Document',
'verbose_name_plural': 'Documents',
'db_table': 'impress_document',
'ordering': ('title',),
"verbose_name": "Document",
"verbose_name_plural": "Documents",
"db_table": "impress_document",
"ordering": ("title",),
},
),
migrations.CreateModel(
name='Template',
name="Template",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('title', models.CharField(max_length=255, verbose_name='title')),
('description', models.TextField(blank=True, verbose_name='description')),
('code', models.TextField(blank=True, verbose_name='code')),
('css', models.TextField(blank=True, verbose_name='css')),
('is_public', models.BooleanField(default=False, help_text='Whether this template is public for anyone to use.', verbose_name='public')),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("title", models.CharField(max_length=255, verbose_name="title")),
(
"description",
models.TextField(blank=True, verbose_name="description"),
),
("code", models.TextField(blank=True, verbose_name="code")),
("css", models.TextField(blank=True, verbose_name="css")),
(
"is_public",
models.BooleanField(
default=False,
help_text="Whether this template is public for anyone to use.",
verbose_name="public",
),
),
],
options={
'verbose_name': 'Template',
'verbose_name_plural': 'Templates',
'db_table': 'impress_template',
'ordering': ('title',),
"verbose_name": "Template",
"verbose_name_plural": "Templates",
"db_table": "impress_template",
"ordering": ("title",),
},
),
migrations.CreateModel(
name='User',
name="User",
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('sub', models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.', regex='^[\\w.@+-]+\\Z')], verbose_name='sub')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')),
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')),
('language', models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
('timezone', timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='UTC', help_text='The timezone in which the user wants to see times.', use_pytz=False)),
('is_device', models.BooleanField(default=False, help_text='Whether the user is a device or a real user.', verbose_name='device')),
('is_staff', models.BooleanField(default=False, help_text='Whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"sub",
models.CharField(
blank=True,
help_text="Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.",
max_length=255,
null=True,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.",
regex="^[\\w.@+-]+\\Z",
)
],
verbose_name="sub",
),
),
(
"email",
models.EmailField(
blank=True,
max_length=254,
null=True,
verbose_name="identity email address",
),
),
(
"admin_email",
models.EmailField(
blank=True,
max_length=254,
null=True,
unique=True,
verbose_name="admin email address",
),
),
(
"language",
models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
(
"timezone",
timezone_field.fields.TimeZoneField(
choices_display="WITH_GMT_OFFSET",
default="UTC",
help_text="The timezone in which the user wants to see times.",
use_pytz=False,
),
),
(
"is_device",
models.BooleanField(
default=False,
help_text="Whether the user is a device or a real user.",
verbose_name="device",
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'db_table': 'impress_user',
"verbose_name": "user",
"verbose_name_plural": "users",
"db_table": "impress_user",
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
("objects", django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='DocumentAccess',
name="DocumentAccess",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('team', models.CharField(blank=True, max_length=100)),
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.document')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("team", models.CharField(blank=True, max_length=100)),
(
"role",
models.CharField(
choices=[
("reader", "Reader"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="accesses",
to="core.document",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'verbose_name': 'Document/user relation',
'verbose_name_plural': 'Document/user relations',
'db_table': 'impress_document_access',
'ordering': ('-created_at',),
"verbose_name": "Document/user relation",
"verbose_name_plural": "Document/user relations",
"db_table": "impress_document_access",
"ordering": ("-created_at",),
},
),
migrations.CreateModel(
name='Invitation',
name="Invitation",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('email', models.EmailField(max_length=254, verbose_name='email address')),
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.document')),
('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"email",
models.EmailField(max_length=254, verbose_name="email address"),
),
(
"role",
models.CharField(
choices=[
("reader", "Reader"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="invitations",
to="core.document",
),
),
(
"issuer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="invitations",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'verbose_name': 'Document invitation',
'verbose_name_plural': 'Document invitations',
'db_table': 'impress_invitation',
"verbose_name": "Document invitation",
"verbose_name_plural": "Document invitations",
"db_table": "impress_invitation",
},
),
migrations.CreateModel(
name='TemplateAccess',
name="TemplateAccess",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('team', models.CharField(blank=True, max_length=100)),
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.template')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
("team", models.CharField(blank=True, max_length=100)),
(
"role",
models.CharField(
choices=[
("reader", "Reader"),
("editor", "Editor"),
("administrator", "Administrator"),
("owner", "Owner"),
],
default="reader",
max_length=20,
),
),
(
"template",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="accesses",
to="core.template",
),
),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'verbose_name': 'Template/user relation',
'verbose_name_plural': 'Template/user relations',
'db_table': 'impress_template_access',
'ordering': ('-created_at',),
"verbose_name": "Template/user relation",
"verbose_name_plural": "Template/user relations",
"db_table": "impress_template_access",
"ordering": ("-created_at",),
},
),
migrations.AddConstraint(
model_name='documentaccess',
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'document'), name='unique_document_user', violation_error_message='This user is already in this document.'),
model_name="documentaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("user__isnull", False)),
fields=("user", "document"),
name="unique_document_user",
violation_error_message="This user is already in this document.",
),
),
migrations.AddConstraint(
model_name='documentaccess',
constraint=models.UniqueConstraint(condition=models.Q(('team__gt', '')), fields=('team', 'document'), name='unique_document_team', violation_error_message='This team is already in this document.'),
model_name="documentaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("team__gt", "")),
fields=("team", "document"),
name="unique_document_team",
violation_error_message="This team is already in this document.",
),
),
migrations.AddConstraint(
model_name='documentaccess',
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
model_name="documentaccess",
constraint=models.CheckConstraint(
check=models.Q(
models.Q(("team", ""), ("user__isnull", False)),
models.Q(("team__gt", ""), ("user__isnull", True)),
_connector="OR",
),
name="check_document_access_either_user_or_team",
violation_error_message="Either user or team must be set, not both.",
),
),
migrations.AddConstraint(
model_name='invitation',
constraint=models.UniqueConstraint(fields=('email', 'document'), name='email_and_document_unique_together'),
model_name="invitation",
constraint=models.UniqueConstraint(
fields=("email", "document"), name="email_and_document_unique_together"
),
),
migrations.AddConstraint(
model_name='templateaccess',
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'template'), name='unique_template_user', violation_error_message='This user is already in this template.'),
model_name="templateaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("user__isnull", False)),
fields=("user", "template"),
name="unique_template_user",
violation_error_message="This user is already in this template.",
),
),
migrations.AddConstraint(
model_name='templateaccess',
constraint=models.UniqueConstraint(condition=models.Q(('team__gt', '')), fields=('team', 'template'), name='unique_template_team', violation_error_message='This team is already in this template.'),
model_name="templateaccess",
constraint=models.UniqueConstraint(
condition=models.Q(("team__gt", "")),
fields=("team", "template"),
name="unique_template_team",
violation_error_message="This team is already in this template.",
),
),
migrations.AddConstraint(
model_name='templateaccess',
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
model_name="templateaccess",
constraint=models.CheckConstraint(
check=models.Q(
models.Q(("team", ""), ("user__isnull", False)),
models.Q(("team__gt", ""), ("user__isnull", True)),
_connector="OR",
),
name="check_template_access_either_user_or_team",
violation_error_message="Either user or team must be set, not both.",
),
),
]

View File

@@ -1,9 +1,9 @@
from django.db import migrations
class Migration(migrations.Migration):
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
("core", "0001_initial"),
]
operations = [

View File

@@ -1,52 +1,114 @@
# Generated by Django 5.1 on 2024-09-08 16:55
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_create_pg_trgm_extension'),
("core", "0002_create_pg_trgm_extension"),
]
operations = [
migrations.AddField(
model_name='document',
name='link_reach',
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='authenticated', max_length=20),
model_name="document",
name="link_reach",
field=models.CharField(
choices=[
("restricted", "Restricted"),
("authenticated", "Authenticated"),
("public", "Public"),
],
default="authenticated",
max_length=20,
),
),
migrations.AddField(
model_name='document',
name='link_role',
field=models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor')], default='reader', max_length=20),
model_name="document",
name="link_role",
field=models.CharField(
choices=[("reader", "Reader"), ("editor", "Editor")],
default="reader",
max_length=20,
),
),
migrations.AlterField(
model_name='document',
name='is_public',
model_name="document",
name="is_public",
field=models.BooleanField(null=True),
),
migrations.AlterField(
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
migrations.CreateModel(
name='LinkTrace',
name="LinkTrace",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to='core.document')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='link_traces', to=settings.AUTH_USER_MODEL)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="link_traces",
to="core.document",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="link_traces",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'verbose_name': 'Document/user link trace',
'verbose_name_plural': 'Document/user link traces',
'db_table': 'impress_link_trace',
'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_link_trace_document_user', violation_error_message='A link trace already exists for this document/user.')],
"verbose_name": "Document/user link trace",
"verbose_name_plural": "Document/user link traces",
"db_table": "impress_link_trace",
"constraints": [
models.UniqueConstraint(
fields=("user", "document"),
name="unique_link_trace_document_user",
violation_error_message="A link trace already exists for this document/user.",
)
],
},
),
]

View File

@@ -1,13 +1,14 @@
# Generated by Django 5.1 on 2024-09-08 17:04
from django.db import migrations
def migrate_is_public_to_link_reach(apps, schema_editor):
"""
Forward migration: Migrate 'is_public' to 'link_reach'.
If is_public == True, set link_reach to 'public'
"""
Document = apps.get_model('core', 'Document')
Document.objects.filter(is_public=True).update(link_reach='public')
Document = apps.get_model("core", "Document")
Document.objects.filter(is_public=True).update(link_reach="public")
def reverse_migrate_link_reach_to_is_public(apps, schema_editor):
@@ -16,20 +17,20 @@ def reverse_migrate_link_reach_to_is_public(apps, schema_editor):
- If link_reach == 'public', set is_public to True
- Else set is_public to False
"""
Document = apps.get_model('core', 'Document')
Document.objects.filter(link_reach='public').update(is_public=True)
Document.objects.filter(link_reach__in=['restricted', "authenticated"]).update(is_public=False)
Document = apps.get_model("core", "Document")
Document.objects.filter(link_reach="public").update(is_public=True)
Document.objects.filter(link_reach__in=["restricted", "authenticated"]).update(
is_public=False
)
class Migration(migrations.Migration):
dependencies = [
('core', '0003_document_link_reach_document_link_role_and_more'),
("core", "0003_document_link_reach_document_link_role_and_more"),
]
operations = [
migrations.RunPython(
migrate_is_public_to_link_reach,
reverse_migrate_link_reach_to_is_public
migrate_is_public_to_link_reach, reverse_migrate_link_reach_to_is_public
),
]

View File

@@ -4,15 +4,16 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_migrate_is_public_to_link_reach'),
("core", "0004_migrate_is_public_to_link_reach"),
]
operations = [
migrations.AlterField(
model_name='document',
name='title',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title'),
model_name="document",
name="title",
field=models.CharField(
blank=True, max_length=255, null=True, verbose_name="title"
),
),
]

View File

@@ -4,25 +4,34 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_remove_document_is_public_alter_document_link_reach_and_more'),
("core", "0005_remove_document_is_public_alter_document_link_reach_and_more"),
]
operations = [
migrations.AddField(
model_name='user',
name='full_name',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='full name'),
model_name="user",
name="full_name",
field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="full name"
),
),
migrations.AddField(
model_name='user',
name='short_name',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='short name'),
model_name="user",
name="short_name",
field=models.CharField(
blank=True, max_length=20, null=True, verbose_name="short name"
),
),
migrations.AlterField(
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
]

View File

@@ -117,10 +117,10 @@ BEGIN
END $$;
"""
class Migration(migrations.Migration):
class Migration(migrations.Migration):
dependencies = [
('core', '0006_add_user_full_name_and_short_name'),
("core", "0006_add_user_full_name_and_short_name"),
]
operations = [

View File

@@ -4,15 +4,22 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_fix_users_duplicate'),
("core", "0007_fix_users_duplicate"),
]
operations = [
migrations.AlterField(
model_name='document',
name='link_reach',
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='restricted', max_length=20),
model_name="document",
name="link_reach",
field=models.CharField(
choices=[
("restricted", "Restricted"),
("authenticated", "Authenticated"),
("public", "Public"),
],
default="restricted",
max_length=20,
),
),
]

View File

@@ -1,37 +1,87 @@
# Generated by Django 5.1.2 on 2024-11-08 07:59
import django.db.models.deletion
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_alter_document_link_reach'),
("core", "0008_alter_document_link_reach"),
]
operations = [
migrations.AlterField(
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
migrations.CreateModel(
name='DocumentFavorite',
name="DocumentFavorite",
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by_users', to='core.document')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_documents', to=settings.AUTH_USER_MODEL)),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="primary key for the record as UUID",
primary_key=True,
serialize=False,
verbose_name="id",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True,
help_text="date and time at which a record was created",
verbose_name="created on",
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True,
help_text="date and time at which a record was last updated",
verbose_name="updated on",
),
),
(
"document",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="favorited_by_users",
to="core.document",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="favorite_documents",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'verbose_name': 'Document favorite',
'verbose_name_plural': 'Document favorites',
'db_table': 'impress_document_favorite',
'constraints': [models.UniqueConstraint(fields=('user', 'document'), name='unique_document_favorite_user', violation_error_message='This document is already targeted by a favorite relation instance for the same user.')],
"verbose_name": "Document favorite",
"verbose_name_plural": "Document favorites",
"db_table": "impress_document_favorite",
"constraints": [
models.UniqueConstraint(
fields=("user", "document"),
name="unique_document_favorite_user",
violation_error_message="This document is already targeted by a favorite relation instance for the same user.",
)
],
},
),
]

View File

@@ -7,25 +7,48 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0009_add_document_favorite'),
("core", "0009_add_document_favorite"),
]
operations = [
migrations.AddField(
model_name='document',
name='creator',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL),
model_name="document",
name="creator",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="documents_created",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
migrations.AlterField(
model_name='user',
name='sub',
field=models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.', regex='^[\\w.@+-:]+\\Z')], verbose_name='sub'),
model_name="user",
name="sub",
field=models.CharField(
blank=True,
help_text="Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only.",
max_length=255,
null=True,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters.",
regex="^[\\w.@+-:]+\\Z",
)
],
verbose_name="sub",
),
),
]

View File

@@ -3,7 +3,7 @@
import django.db.models.deletion
from django.conf import settings
from django.db import migrations
from django.db.models import F, ForeignKey, Subquery, OuterRef, Q
from django.db.models import F, ForeignKey, OuterRef, Q, Subquery
def set_creator_from_document_access(apps, schema_editor):
@@ -25,28 +25,37 @@ def set_creator_from_document_access(apps, schema_editor):
DocumentAccess = apps.get_model("core", "DocumentAccess")
# Update `creator` using the "owner" role
owner_subquery = DocumentAccess.objects.filter(
document=OuterRef('pk'),
user__isnull=False,
role='owner',
).order_by('created_at').values('user_id')[:1]
owner_subquery = (
DocumentAccess.objects.filter(
document=OuterRef("pk"),
user__isnull=False,
role="owner",
)
.order_by("created_at")
.values("user_id")[:1]
)
Document.objects.filter(
creator__isnull=True
).update(creator=Subquery(owner_subquery))
Document.objects.filter(creator__isnull=True).update(
creator=Subquery(owner_subquery)
)
class Migration(migrations.Migration):
dependencies = [
('core', '0010_add_field_creator_to_document'),
("core", "0010_add_field_creator_to_document"),
]
operations = [
migrations.RunPython(set_creator_from_document_access, reverse_code=migrations.RunPython.noop),
migrations.RunPython(
set_creator_from_document_access, reverse_code=migrations.RunPython.noop
),
migrations.AlterField(
model_name='document',
name='creator',
field=ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL),
model_name="document",
name="creator",
field=ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="documents_created",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@@ -6,25 +6,42 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0011_populate_creator_field_and_make_it_required'),
("core", "0011_populate_creator_field_and_make_it_required"),
]
operations = [
migrations.AlterField(
model_name='document',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='documents_created', to=settings.AUTH_USER_MODEL),
model_name="document",
name="creator",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="documents_created",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name='invitation',
name='issuer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL),
model_name="invitation",
name="issuer",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="invitations",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name='user',
name='language',
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 5.1.4 on 2025-01-25 08:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0012_make_document_creator_and_invitation_issuer_optional"),
]
operations = [
migrations.RunSQL(
"CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;",
reverse_sql="DROP EXTENSION IF EXISTS fuzzystrmatch;",
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.1.2 on 2024-12-07 09:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0013_activate_fuzzystrmatch_extension"),
]
operations = [
migrations.AddField(
model_name="document",
name="depth",
field=models.PositiveIntegerField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name="document",
name="numchild",
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name="document",
name="path",
# Allow null values pending the next datamigration to populate the field
field=models.CharField(
db_collation="C", max_length=252, null=True, unique=True
),
preserve_default=False,
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 5.1.2 on 2024-12-07 10:33
from django.db import migrations, models
from treebeard.numconv import NumConv
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
STEPLEN = 7
def set_path_on_existing_documents(apps, schema_editor):
"""
Updates the `path` and `depth` fields for all existing Document records
to ensure valid materialized paths.
This function assigns a unique `path` to each Document as a root node
Note: After running this migration, we quickly modify the schema to make
the `path` field required as it should.
"""
Document = apps.get_model("core", "Document")
# Iterate over all existing documents and make them root nodes
documents = Document.objects.order_by("created_at").values_list("id", flat=True)
numconv = NumConv(len(ALPHABET), ALPHABET)
updates = []
for i, pk in enumerate(documents):
key = numconv.int2str(i)
path = "{0}{1}".format(ALPHABET[0] * (STEPLEN - len(key)), key)
updates.append(Document(pk=pk, path=path, depth=1))
# Bulk update using the prepared updates list
Document.objects.bulk_update(updates, ["depth", "path"])
class Migration(migrations.Migration):
dependencies = [
("core", "0014_add_tree_structure_to_documents"),
]
operations = [
migrations.RunPython(
set_path_on_existing_documents, reverse_code=migrations.RunPython.noop
),
migrations.AlterField(
model_name="document",
name="path",
field=models.CharField(db_collation="C", max_length=252, unique=True),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.1.4 on 2024-12-18 08:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0015_set_path_on_existing_documents"),
]
operations = [
migrations.AddField(
model_name="document",
name="excerpt",
field=models.TextField(
blank=True, max_length=300, null=True, verbose_name="excerpt"
),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.1.4 on 2025-01-12 14:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0016_add_document_excerpt"),
]
operations = [
migrations.AlterModelOptions(
name="document",
options={
"ordering": ("path",),
"verbose_name": "Document",
"verbose_name_plural": "Documents",
},
),
migrations.AddField(
model_name="document",
name="ancestors_deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="document",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
choices="(('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'))",
default="en-us",
help_text="The language in which the user wants to see the interface.",
max_length=10,
verbose_name="language",
),
),
migrations.AddConstraint(
model_name="document",
constraint=models.CheckConstraint(
condition=models.Q(
("deleted_at__isnull", True),
("deleted_at", models.F("ancestors_deleted_at")),
_connector="OR",
),
name="check_deleted_at_matches_ancestors_deleted_at_when_set",
),
),
]

View File

@@ -0,0 +1,24 @@
from django.db import migrations
def update_titles_to_null(apps, schema_editor):
"""
If the titles are "Untitled document" or "Unbenanntes Dokument" or "Document sans titre"
we set them to Null
"""
Document = apps.get_model("core", "Document")
Document.objects.filter(
title__in=["Untitled document", "Unbenanntes Dokument", "Document sans titre"]
).update(title=None)
class Migration(migrations.Migration):
dependencies = [
("core", "0017_add_fields_for_soft_delete"),
]
operations = [
migrations.RunPython(
update_titles_to_null, reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.1.5 on 2025-03-04 12:23
from django.db import migrations, models
import core.models
class Migration(migrations.Migration):
dependencies = [
("core", "0018_update_blank_title"),
]
operations = [
migrations.AlterModelManagers(
name="user",
managers=[
("objects", core.models.UserManager()),
],
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("fr-fr", "Français"),
("de-de", "Deutsch"),
],
default=None,
help_text="The language in which the user wants to see the interface.",
max_length=10,
null=True,
verbose_name="language",
),
),
]

View File

@@ -0,0 +1,77 @@
# Generated by Django 5.1.4 on 2025-01-18 11:53
import re
import django.contrib.postgres.fields
import django.db.models.deletion
from django.core.files.storage import default_storage
from django.db import migrations, models
from botocore.exceptions import ClientError
import core.models
from core.utils import extract_attachments
def populate_attachments_on_all_documents(apps, schema_editor):
"""Populate "attachments" field on all existing documents in the database."""
Document = apps.get_model("core", "Document")
for document in Document.objects.all():
try:
response = default_storage.connection.meta.client.get_object(
Bucket=default_storage.bucket_name, Key=f"{document.pk!s}/file"
)
except (FileNotFoundError, ClientError):
pass
else:
content = response["Body"].read().decode("utf-8")
document.attachments = extract_attachments(content)
document.save(update_fields=["attachments"])
class Migration(migrations.Migration):
dependencies = [
("core", "0019_alter_user_language_default_to_null"),
]
operations = [
# v2.0.0 was released so we can now remove BC field "is_public"
migrations.RemoveField(
model_name="document",
name="is_public",
),
migrations.AlterModelManagers(
name="user",
managers=[
("objects", core.models.UserManager()),
],
),
migrations.AddField(
model_name="document",
name="attachments",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=255),
blank=True,
default=list,
editable=False,
null=True,
size=None,
),
),
migrations.AddField(
model_name="document",
name="duplicated_from",
field=models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="duplicates",
to="core.document",
),
),
migrations.RunPython(
populate_attachments_on_all_documents,
reverse_code=migrations.RunPython.noop,
),
]

View File

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

View File

@@ -1,59 +1,53 @@
"""
Declare and configure the models for the impress core application
"""
# pylint: disable=too-many-lines
import hashlib
import smtplib
import tempfile
import textwrap
import uuid
from collections import defaultdict
from datetime import timedelta
from io import BytesIO
from logging import getLogger
from django.conf import settings
from django.contrib.auth import models as auth_models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.postgres.fields import ArrayField
from django.contrib.sites.models import Site
from django.core import exceptions, mail, validators
from django.core import mail, validators
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.mail import send_mail
from django.db import models
from django.http import FileResponse
from django.template.base import Template as DjangoTemplate
from django.template.context import Context
from django.db import models, transaction
from django.db.models.functions import Left, Length
from django.template.loader import render_to_string
from django.utils import html, timezone
from django.utils.functional import cached_property, lazy
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import get_language, override
from django.utils.translation import gettext_lazy as _
import frontmatter
import markdown
import pypandoc
import weasyprint
from botocore.exceptions import ClientError
from rest_framework.exceptions import ValidationError
from timezone_field import TimeZoneField
from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
logger = getLogger(__name__)
def get_resource_roles(resource, user):
"""Compute the roles a user has on a resource."""
if not user.is_authenticated:
return []
def get_trashbin_cutoff():
"""
Calculate the cutoff datetime for soft-deleted items based on the retention policy.
try:
roles = resource.user_roles or []
except AttributeError:
try:
roles = resource.accesses.filter(
models.Q(user=user) | models.Q(team__in=user.teams),
).values_list("role", flat=True)
except (models.ObjectDoesNotExist, IndexError):
roles = []
return roles
The function returns the current datetime minus the number of days specified in
the TRASHBIN_CUTOFF_DAYS setting, indicating the oldest date for items that can
remain in the trash bin.
Returns:
datetime: The cutoff datetime for soft-deleted items.
"""
return timezone.now() - timedelta(days=settings.TRASHBIN_CUTOFF_DAYS)
class LinkRoleChoices(models.TextChoices):
@@ -88,6 +82,65 @@ class LinkReachChoices(models.TextChoices):
) # Any authenticated user can access the document
PUBLIC = "public", _("Public") # Even anonymous users can access the document
@classmethod
def get_select_options(cls, ancestors_links):
"""
Determines the valid select options for link reach and link role depending on the
list of ancestors' link reach/role.
Args:
ancestors_links: List of dictionaries, each with 'link_reach' and 'link_role' keys
representing the reach and role of ancestors links.
Returns:
Dictionary mapping possible reach levels to their corresponding possible roles.
"""
# If no ancestors, return all options
if not ancestors_links:
return dict.fromkeys(cls.values, LinkRoleChoices.values)
# Initialize result with all possible reaches and role options as sets
result = {reach: set(LinkRoleChoices.values) for reach in cls.values}
# Group roles by reach level
reach_roles = defaultdict(set)
for link in ancestors_links:
reach_roles[link["link_reach"]].add(link["link_role"])
# Apply constraints based on ancestor links
if LinkRoleChoices.EDITOR in reach_roles[cls.RESTRICTED]:
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
if LinkRoleChoices.EDITOR in reach_roles[cls.AUTHENTICATED]:
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
result.pop(cls.RESTRICTED, None)
elif LinkRoleChoices.READER in reach_roles[cls.AUTHENTICATED]:
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
if LinkRoleChoices.EDITOR in reach_roles[cls.PUBLIC]:
result[cls.PUBLIC].discard(LinkRoleChoices.READER)
result.pop(cls.AUTHENTICATED, None)
result.pop(cls.RESTRICTED, None)
elif LinkRoleChoices.READER in reach_roles[cls.PUBLIC]:
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
result.get(cls.RESTRICTED, set()).discard(LinkRoleChoices.READER)
# Convert roles sets to lists while maintaining the order from LinkRoleChoices
for reach, roles in result.items():
result[reach] = [role for role in LinkRoleChoices.values if role in roles]
return result
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):
"""
@@ -126,6 +179,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."""
@@ -162,10 +244,12 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
language = models.CharField(
max_length=10,
choices=lazy(lambda: settings.LANGUAGES, tuple)(),
default=settings.LANGUAGE_CODE,
choices=settings.LANGUAGES,
default=None,
verbose_name=_("language"),
help_text=_("The language in which the user wants to see the interface."),
null=True,
blank=True,
)
timezone = TimeZoneField(
choices_display="WITH_GMT_OFFSET",
@@ -192,7 +276,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
),
)
objects = auth_models.UserManager()
objects = UserManager()
USERNAME_FIELD = "admin_email"
REQUIRED_FIELDS = []
@@ -280,10 +364,9 @@ class BaseAccess(BaseModel):
class Meta:
abstract = True
def _get_abilities(self, resource, user):
def _get_roles(self, resource, user):
"""
Compute and return abilities for a given user taking into account
the current state of the object.
Get the roles a user has on a resource.
"""
roles = []
if user.is_authenticated:
@@ -298,6 +381,15 @@ class BaseAccess(BaseModel):
except (self._meta.model.DoesNotExist, IndexError):
roles = []
return roles
def _get_abilities(self, resource, user):
"""
Compute and return abilities for a given user taking into account
the current state of the object.
"""
roles = self._get_roles(resource, user)
is_owner_or_admin = bool(
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
)
@@ -336,10 +428,47 @@ class BaseAccess(BaseModel):
}
class Document(BaseModel):
class DocumentQuerySet(MP_NodeQuerySet):
"""
Custom queryset for the Document model, providing additional methods
to filter documents based on user permissions.
"""
def readable_per_se(self, user):
"""
Filters the queryset to return documents on which the given user has
direct access, team access or link access. This will not return all the
documents that a user can read because it can be obtained via an ancestor.
:param user: The user for whom readable documents are to be fetched.
:return: A queryset of documents for which the user has direct access,
team access or link access.
"""
if user.is_authenticated:
return self.filter(
models.Q(accesses__user=user)
| models.Q(accesses__team__in=user.teams)
| ~models.Q(link_reach=LinkReachChoices.RESTRICTED)
)
return self.filter(link_reach=LinkReachChoices.PUBLIC)
class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
"""
Custom manager for the Document model, enabling the use of the custom
queryset methods directly from the model manager.
"""
def get_queryset(self):
"""Sets the custom queryset as the default."""
return self._queryset_class(self.model).order_by("path")
class Document(MP_Node, BaseModel):
"""Pad document carrying the content."""
title = models.CharField(_("title"), max_length=255, null=True, blank=True)
excerpt = models.TextField(_("excerpt"), max_length=300, null=True, blank=True)
link_reach = models.CharField(
max_length=20,
choices=LinkReachChoices.choices,
@@ -355,14 +484,49 @@ class Document(BaseModel):
blank=True,
null=True,
)
deleted_at = models.DateTimeField(null=True, blank=True)
ancestors_deleted_at = models.DateTimeField(null=True, blank=True)
duplicated_from = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
related_name="duplicates",
editable=False,
blank=True,
null=True,
)
attachments = ArrayField(
models.CharField(max_length=255),
default=list,
editable=False,
blank=True,
null=True,
)
_content = None
# Tree structure
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
steplen = 7 # nb siblings max: 3,521,614,606,208
node_order_by = [] # Manual ordering
path = models.CharField(max_length=7 * 36, unique=True, db_collation="C")
objects = DocumentManager()
class Meta:
db_table = "impress_document"
ordering = ("title",)
ordering = ("path",)
verbose_name = _("Document")
verbose_name_plural = _("Documents")
constraints = [
models.CheckConstraint(
check=(
models.Q(deleted_at__isnull=True)
| models.Q(deleted_at=models.F("ancestors_deleted_at"))
),
name="check_deleted_at_matches_ancestors_deleted_at_when_set",
),
]
def __str__(self):
return str(self.title) if self.title else str(_("Untitled Document"))
@@ -433,9 +597,13 @@ class Document(BaseModel):
def get_content_response(self, version_id=""):
"""Get the content in a specific version of the document"""
return default_storage.connection.meta.client.get_object(
Bucket=default_storage.bucket_name, Key=self.file_key, VersionId=version_id
)
params = {
"Bucket": default_storage.bucket_name,
"Key": self.file_key,
}
if version_id:
params["VersionId"] = version_id
return default_storage.connection.meta.client.get_object(**params)
def get_versions_slice(self, from_version_id="", min_datetime=None, page_size=None):
"""Get document versions from object storage with pagination and starting conditions"""
@@ -501,48 +669,195 @@ class Document(BaseModel):
Bucket=default_storage.bucket_name, Key=self.file_key, VersionId=version_id
)
def get_abilities(self, user):
def get_nb_accesses_cache_key(self):
"""Generate a unique cache key for each document."""
return f"document_{self.id!s}_nb_accesses"
def get_nb_accesses(self):
"""
Calculate the number of accesses:
- directly attached to the document
- attached to any of the document's ancestors
"""
cache_key = self.get_nb_accesses_cache_key()
nb_accesses = cache.get(cache_key)
if nb_accesses is None:
nb_accesses = (
DocumentAccess.objects.filter(document=self).count(),
DocumentAccess.objects.filter(
document__path=Left(
models.Value(self.path), Length("document__path")
),
document__ancestors_deleted_at__isnull=True,
).count(),
)
cache.set(cache_key, nb_accesses)
return nb_accesses
@property
def nb_accesses_direct(self):
"""Returns the number of accesses related to the document or one of its ancestors."""
return self.get_nb_accesses()[0]
@property
def nb_accesses_ancestors(self):
"""Returns the number of accesses related to the document or one of its ancestors."""
return self.get_nb_accesses()[1]
def invalidate_nb_accesses_cache(self):
"""
Invalidate the cache for number of accesses, including on affected descendants.
Args:
path: can optionally be passed as argument (useful when invalidating cache for a
document we just deleted)
"""
for document in Document.objects.filter(path__startswith=self.path).only("id"):
cache_key = document.get_nb_accesses_cache_key()
cache.delete(cache_key)
def get_roles(self, user):
"""Return the roles a user has on a document."""
if not user.is_authenticated:
return []
try:
roles = self.user_roles or []
except AttributeError:
try:
roles = DocumentAccess.objects.filter(
models.Q(user=user) | models.Q(team__in=user.teams),
document__path=Left(
models.Value(self.path), Length("document__path")
),
).values_list("role", flat=True)
except (models.ObjectDoesNotExist, IndexError):
roles = []
return roles
def get_links_definitions(self, ancestors_links):
"""Get links reach/role definitions for the current document and its ancestors."""
links_definitions = defaultdict(set)
links_definitions[self.link_reach].add(self.link_role)
# Merge ancestor link definitions
for ancestor in ancestors_links:
links_definitions[ancestor["link_reach"]].add(ancestor["link_role"])
return dict(links_definitions) # Convert defaultdict back to a normal dict
def compute_ancestors_links(self, user):
"""
Compute the ancestors links for the current document up to the highest readable ancestor.
"""
ancestors = (
(self.get_ancestors() | self._meta.model.objects.filter(pk=self.pk))
.filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
highest_readable = ancestors.readable_per_se(user).only("depth").first()
if highest_readable is None:
return []
ancestors_links = []
paths_links_mapping = {}
for ancestor in ancestors.filter(depth__gte=highest_readable.depth):
ancestors_links.append(
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
)
paths_links_mapping[ancestor.path] = ancestors_links.copy()
ancestors_links = paths_links_mapping.get(self.path[: -self.steplen], [])
return ancestors_links
def get_abilities(self, user, ancestors_links=None):
"""
Compute and return abilities for a given user on the document.
"""
roles = set(get_resource_roles(self, user))
if self.depth <= 1 or getattr(self, "is_highest_ancestor_for_user", False):
ancestors_links = []
elif ancestors_links is None:
ancestors_links = self.compute_ancestors_links(user=user)
# Compute version roles before adding link roles because we don't
roles = set(
self.get_roles(user)
) # at this point only roles based on specific access
# Characteristics that are based only on specific access
is_owner = RoleChoices.OWNER in roles
is_deleted = self.ancestors_deleted_at and not is_owner
is_owner_or_admin = (is_owner or RoleChoices.ADMIN in roles) and not is_deleted
# Compute access roles before adding link roles because we don't
# want anonymous users to access versions (we wouldn't know from
# which date to allow them anyway)
# Anonymous users should also not see document accesses
has_role = bool(roles)
has_access_role = bool(roles) and not is_deleted
can_update_from_access = (
is_owner_or_admin or RoleChoices.EDITOR in roles
) and not is_deleted
# Add role provided by the document link
if self.link_reach == LinkReachChoices.PUBLIC or (
self.link_reach == LinkReachChoices.AUTHENTICATED and user.is_authenticated
):
roles.add(self.link_role)
is_owner_or_admin = bool(
roles.intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
# Add roles provided by the document link, taking into account its ancestors
links_definitions = self.get_links_definitions(ancestors_links)
public_roles = links_definitions.get(LinkReachChoices.PUBLIC, set())
authenticated_roles = (
links_definitions.get(LinkReachChoices.AUTHENTICATED, set())
if user.is_authenticated
else set()
)
roles = roles | public_roles | authenticated_roles
can_get = bool(roles) and not is_deleted
can_update = (
is_owner_or_admin or RoleChoices.EDITOR in roles
) and not is_deleted
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any(
[
ai_allow_reach_from == LinkReachChoices.PUBLIC and can_update,
ai_allow_reach_from == LinkReachChoices.AUTHENTICATED
and user.is_authenticated
and can_update,
ai_allow_reach_from == LinkReachChoices.RESTRICTED
and can_update_from_access,
]
)
can_get = bool(roles)
can_update = is_owner_or_admin or RoleChoices.EDITOR in roles
return {
"accesses_manage": is_owner_or_admin,
"accesses_view": has_role,
"ai_transform": can_update,
"ai_translate": can_update,
"accesses_view": has_access_role,
"ai_transform": ai_access,
"ai_translate": ai_access,
"attachment_upload": can_update,
"media_check": can_get,
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"collaboration_auth": can_get,
"destroy": RoleChoices.OWNER in roles,
"content": can_get,
"cors_proxy": can_get,
"descendants": can_get,
"destroy": is_owner,
"duplicate": can_get,
"favorite": can_get and user.is_authenticated,
"link_configuration": is_owner_or_admin,
"invite_owner": RoleChoices.OWNER in roles,
"invite_owner": is_owner,
"move": is_owner_or_admin and not self.ancestors_deleted_at,
"partial_update": can_update,
"restore": is_owner,
"retrieve": can_get,
"media_auth": can_get,
"link_select_options": LinkReachChoices.get_select_options(ancestors_links),
"tree": can_get,
"update": can_update,
"versions_destroy": is_owner_or_admin,
"versions_list": has_role,
"versions_retrieve": has_role,
"versions_list": has_access_role,
"versions_retrieve": has_access_role,
}
def send_email(self, subject, emails, context=None, language=None):
@@ -556,6 +871,7 @@ class Document(BaseModel):
"document": self,
"domain": domain,
"link": f"{domain}/docs/{self.id}/",
"document_title": self.title or str(_("Untitled Document")),
"logo_img": settings.EMAIL_LOGO_IMG,
}
)
@@ -597,12 +913,90 @@ class Document(BaseModel):
'{name} invited you with the role "{role}" on the following document:'
).format(name=sender_name_email, role=role.lower()),
}
subject = _("{name} shared a document with you: {title}").format(
name=sender_name, title=self.title
subject = (
context["title"]
if not self.title
else _("{name} shared a document with you: {title}").format(
name=sender_name, title=self.title
)
)
self.send_email(subject, [email], context, language)
@transaction.atomic
def soft_delete(self):
"""
Soft delete the document, marking the deletion on descendants.
We still keep the .delete() method untouched for programmatic purposes.
"""
if (
self._meta.model.objects.filter(
models.Q(deleted_at__isnull=False)
| models.Q(ancestors_deleted_at__isnull=False),
pk=self.pk,
).exists()
or self.get_ancestors().filter(deleted_at__isnull=False).exists()
):
raise RuntimeError(
"This document is already deleted or has deleted ancestors."
)
self.ancestors_deleted_at = self.deleted_at = timezone.now()
self.save()
self.invalidate_nb_accesses_cache()
if self.depth > 1:
self._meta.model.objects.filter(pk=self.get_parent().pk).update(
numchild=models.F("numchild") - 1
)
# Mark all descendants as soft deleted
self.get_descendants().filter(ancestors_deleted_at__isnull=True).update(
ancestors_deleted_at=self.ancestors_deleted_at
)
@transaction.atomic
def restore(self):
"""Cancelling a soft delete with checks."""
# This should not happen
if self._meta.model.objects.filter(
pk=self.pk, deleted_at__isnull=True
).exists():
raise RuntimeError("This document is not deleted.")
if self.deleted_at < get_trashbin_cutoff():
raise RuntimeError(
"This document was permanently deleted and cannot be restored."
)
# save the current deleted_at value to exclude it from the descendants update
current_deleted_at = self.deleted_at
# Restore the current document
self.deleted_at = None
# Calculate the minimum `deleted_at` among all ancestors
ancestors_deleted_at = (
self.get_ancestors()
.filter(deleted_at__isnull=False)
.order_by("deleted_at")
.values_list("deleted_at", flat=True)
.first()
)
self.ancestors_deleted_at = ancestors_deleted_at
self.save(update_fields=["deleted_at", "ancestors_deleted_at"])
self.invalidate_nb_accesses_cache()
self.get_descendants().exclude(
models.Q(deleted_at__isnull=False)
| models.Q(ancestors_deleted_at__lt=current_deleted_at)
).update(ancestors_deleted_at=self.ancestors_deleted_at)
if self.depth > 1:
self._meta.model.objects.filter(pk=self.get_parent().pk).update(
numchild=models.F("numchild") + 1
)
class LinkTrace(BaseModel):
"""
@@ -705,11 +1099,55 @@ class DocumentAccess(BaseAccess):
def __str__(self):
return f"{self.user!s} is {self.role:s} in document {self.document!s}"
def save(self, *args, **kwargs):
"""Override save to clear the document's cache for number of accesses."""
super().save(*args, **kwargs)
self.document.invalidate_nb_accesses_cache()
def delete(self, *args, **kwargs):
"""Override delete to clear the document's cache for number of accesses."""
super().delete(*args, **kwargs)
self.document.invalidate_nb_accesses_cache()
def get_abilities(self, user):
"""
Compute and return abilities for a given user on the document access.
"""
return self._get_abilities(self.document, user)
roles = self._get_roles(self.document, user)
is_owner_or_admin = bool(set(roles).intersection(set(PRIVILEGED_ROLES)))
if self.role == RoleChoices.OWNER:
can_delete = (
RoleChoices.OWNER in roles
and self.document.accesses.filter(role=RoleChoices.OWNER).count() > 1
)
set_role_to = (
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
if can_delete
else []
)
else:
can_delete = is_owner_or_admin
set_role_to = []
if RoleChoices.OWNER in roles:
set_role_to.append(RoleChoices.OWNER)
if is_owner_or_admin:
set_role_to.extend(
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
)
# Remove the current role as we don't want to propose it as an option
try:
set_role_to.remove(self.role)
except ValueError:
pass
return {
"destroy": can_delete,
"update": bool(set_role_to) and is_owner_or_admin,
"partial_update": bool(set_role_to) and is_owner_or_admin,
"retrieve": self.user and self.user.id == user.id or is_owner_or_admin,
"set_role_to": set_role_to,
}
class Template(BaseModel):
@@ -734,11 +1172,27 @@ class Template(BaseModel):
def __str__(self):
return self.title
def get_roles(self, user):
"""Return the roles a user has on a resource as an iterable."""
if not user.is_authenticated:
return []
try:
roles = self.user_roles or []
except AttributeError:
try:
roles = self.accesses.filter(
models.Q(user=user) | models.Q(team__in=user.teams),
).values_list("role", flat=True)
except (models.ObjectDoesNotExist, IndexError):
roles = []
return roles
def get_abilities(self, user):
"""
Compute and return abilities for a given user on the template.
"""
roles = get_resource_roles(self, user)
roles = self.get_roles(user)
is_owner_or_admin = bool(
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
)
@@ -754,107 +1208,6 @@ class Template(BaseModel):
"retrieve": can_get,
}
def generate_pdf(self, body_html, metadata):
"""
Generate and return a pdf document wrapped around the current template
"""
document_html = weasyprint.HTML(
string=DjangoTemplate(self.code).render(
Context({"body": html.format_html(body_html), **metadata})
)
)
css = weasyprint.CSS(
string=self.css,
font_config=weasyprint.text.fonts.FontConfiguration(),
)
pdf_content = document_html.write_pdf(stylesheets=[css], zoom=1)
response = FileResponse(BytesIO(pdf_content), content_type="application/pdf")
response["Content-Disposition"] = f"attachment; filename={self.title}.pdf"
return response
def generate_word(self, body_html, metadata):
"""
Generate and return a docx document wrapped around the current template
"""
template_string = DjangoTemplate(self.code).render(
Context({"body": html.format_html(body_html), **metadata})
)
html_string = f"""
<!DOCTYPE html>
<html>
<head>
<style>
{self.css}
</style>
</head>
<body>
{template_string}
</body>
</html>
"""
reference_docx = "core/static/reference.docx"
output = BytesIO()
# Convert the HTML to a temporary docx file
with tempfile.NamedTemporaryFile(suffix=".docx", prefix="docx_") as tmp_file:
output_path = tmp_file.name
pypandoc.convert_text(
html_string,
"docx",
format="html",
outputfile=output_path,
extra_args=["--reference-doc", reference_docx],
)
# Create a BytesIO object to store the output of the temporary docx file
with open(output_path, "rb") as f:
output = BytesIO(f.read())
# Ensure the pointer is at the beginning
output.seek(0)
response = FileResponse(
output,
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
response["Content-Disposition"] = f"attachment; filename={self.title}.docx"
return response
def generate_document(self, body, body_type, export_format):
"""
Generate and return a document for this template around the
body passed as argument.
2 types of body are accepted:
- HTML: body_type = "html"
- Markdown: body_type = "markdown"
2 types of documents can be generated:
- PDF: export_format = "pdf"
- Docx: export_format = "docx"
"""
document = frontmatter.loads(body)
metadata = document.metadata
strip_body = document.content.strip()
if body_type == "html":
body_html = strip_body
else:
body_html = (
markdown.markdown(textwrap.dedent(strip_body)) if strip_body else ""
)
if export_format == "pdf":
return self.generate_pdf(body_html, metadata)
return self.generate_word(body_html, metadata)
class TemplateAccess(BaseAccess):
"""Relation model to give access to a template for a user or a team with a role."""
@@ -939,9 +1292,12 @@ class Invitation(BaseModel):
super().clean()
# Check if an identity already exists for the provided email
if User.objects.filter(email=self.email).exists():
raise exceptions.ValidationError(
{"email": _("This email is already associated to a registered user.")}
if (
User.objects.filter(email=self.email).exists()
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
):
raise ValidationError(
{"email": [_("This email is already associated to a registered user.")]}
)
@property

View File

@@ -1,8 +1,5 @@
"""AI services."""
import json
import re
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
@@ -12,32 +9,44 @@ from core import enums
AI_ACTIONS = {
"prompt": (
"Answer the prompt in markdown format. Return JSON: "
'{"answer": "Your markdown answer"}. '
"Do not provide any other information."
"Answer the prompt in markdown format. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"correct": (
"Correct grammar and spelling of the markdown text, "
"preserving language and markdown formatting. "
'Return JSON: {"answer": "your corrected markdown text"}. '
"Do not provide any other information."
"Do not provide any other information. "
"Preserve the language."
),
"rephrase": (
"Rephrase the given markdown text, "
"preserving language and markdown formatting. "
'Return JSON: {"answer": "your rephrased markdown text"}. '
"Do not provide any other information."
"Do not provide any other information. "
"Preserve the language."
),
"summarize": (
"Summarize the markdown text, preserving language and markdown formatting. "
'Return JSON: {"answer": "your markdown summary"}. '
"Do not provide any other information."
"Do not provide any other information. "
"Preserve the language."
),
"beautify": (
"Add formatting to the text to make it more readable. "
"Do not provide any other information. "
"Preserve the language."
),
"emojify": (
"Add emojis to the important parts of the text. "
"Do not provide any other information. "
"Preserve the language."
),
}
AI_TRANSLATE = (
"Translate the markdown text to {language:s}, preserving markdown formatting. "
'Return JSON: {{"answer": "your translated markdown text in {language:s}"}}. '
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language {language:s}. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
)
@@ -59,32 +68,18 @@ class AIService:
"""Helper method to call the OpenAI API and process the response."""
response = self.client.chat.completions.create(
model=settings.AI_MODEL,
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": system_content},
{"role": "user", "content": json.dumps({"markdown_input": text})},
{"role": "user", "content": text},
],
)
content = response.choices[0].message.content
try:
sanitized_content = re.sub(r'\s*"answer"\s*:\s*', '"answer": ', content)
sanitized_content = re.sub(r"\s*\}", "}", sanitized_content)
sanitized_content = re.sub(r"(?<!\\)\n", "\\\\n", sanitized_content)
sanitized_content = re.sub(r"(?<!\\)\t", "\\\\t", sanitized_content)
json_response = json.loads(sanitized_content)
except (json.JSONDecodeError, IndexError):
try:
json_response = json.loads(content)
except json.JSONDecodeError as err:
raise RuntimeError("AI response is not valid JSON", content) from err
if "answer" not in json_response:
if not content:
raise RuntimeError("AI response does not contain an answer")
return json_response
return {"answer": content}
def transform(self, text, action):
"""Transform text based on specified action."""

View File

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

View File

@@ -1,5 +1,6 @@
"""Unit tests for the Authentication Backends."""
import random
import re
from django.core.exceptions import SuspiciousOperation
@@ -7,6 +8,8 @@ from django.test.utils import override_settings
import pytest
import responses
from cryptography.fernet import Fernet
from lasuite.oidc_login.backends import get_oidc_refresh_token
from core import models
from core.authentication.backends import OIDCAuthenticationBackend
@@ -54,7 +57,7 @@ def test_authentication_getter_existing_user_via_email(
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with django_assert_num_queries(2):
with django_assert_num_queries(3): # user by sub, user by mail, update sub
user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
@@ -62,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
):
"""
@@ -75,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}
@@ -91,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
):
@@ -128,11 +191,12 @@ def test_authentication_getter_existing_user_with_email(
("Jack", "Duy", "jack.duy@example.com"),
],
)
def test_authentication_getter_existing_user_change_fields(
def test_authentication_getter_existing_user_change_fields_sub(
first_name, last_name, email, django_assert_num_queries, monkeypatch
):
"""
It should update the email or name fields on the user when they change.
It should update the email or name fields on the user when they change
and the user was identified by its "sub".
"""
klass = OIDCAuthenticationBackend()
user = UserFactory(
@@ -162,6 +226,48 @@ def test_authentication_getter_existing_user_change_fields(
assert user.short_name == first_name
@pytest.mark.parametrize(
"first_name, last_name, email",
[
("Jack", "Doe", "john.doe@example.com"),
("John", "Duy", "john.doe@example.com"),
],
)
def test_authentication_getter_existing_user_change_fields_email(
first_name, last_name, email, django_assert_num_queries, monkeypatch
):
"""
It should update the name fields on the user when they change
and the user was identified by its "email" as fallback.
"""
klass = OIDCAuthenticationBackend()
user = UserFactory(
full_name="John Doe", short_name="John", email="john.doe@example.com"
)
def get_userinfo_mocked(*args):
return {
"sub": "123",
"email": user.email,
"first_name": first_name,
"last_name": last_name,
}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
# One and only one additional update query when a field has changed
with django_assert_num_queries(3):
authenticated_user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
assert user == authenticated_user
user.refresh_from_db()
assert user.email == email
assert user.full_name == f"{first_name:s} {last_name:s}"
assert user.short_name == first_name
def test_authentication_getter_new_user_no_email(monkeypatch):
"""
If no user matches the user's info sub, a user should be created.
@@ -182,7 +288,7 @@ def test_authentication_getter_new_user_no_email(monkeypatch):
assert user.email is None
assert user.full_name is None
assert user.short_name is None
assert user.password == "!"
assert user.has_usable_password() is False
assert models.User.objects.count() == 1
@@ -209,33 +315,10 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
assert user.email == email
assert user.full_name == "John Doe"
assert user.short_name == "John"
assert user.password == "!"
assert user.has_usable_password() is False
assert models.User.objects.count() == 1
def test_authentication_getter_invalid_token(django_assert_num_queries, monkeypatch):
"""The user's info doesn't contain a sub."""
klass = OIDCAuthenticationBackend()
def get_userinfo_mocked(*args):
return {
"test": "123",
}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with (
django_assert_num_queries(0),
pytest.raises(
SuspiciousOperation,
match="User info contained no recognizable user identification",
),
):
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
assert models.User.objects.exists() is False
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
@responses.activate
def test_authentication_get_userinfo_json_response():
@@ -262,11 +345,15 @@ def test_authentication_get_userinfo_json_response():
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
@responses.activate
def test_authentication_get_userinfo_token_response(monkeypatch):
def test_authentication_get_userinfo_token_response(monkeypatch, settings):
"""Test get_userinfo method with a token response."""
settings.OIDC_RP_SIGN_ALGO = "HS256" # disable JWKS URL call
responses.add(
responses.GET, re.compile(r".*/userinfo"), body="fake.jwt.token", status=200
responses.GET,
re.compile(r".*/userinfo"),
body="fake.jwt.token",
status=200,
content_type="application/jwt",
)
def mock_verify_token(self, token): # pylint: disable=unused-argument
@@ -288,21 +375,25 @@ def test_authentication_get_userinfo_token_response(monkeypatch):
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
@responses.activate
def test_authentication_get_userinfo_invalid_response():
def test_authentication_get_userinfo_invalid_response(settings):
"""
Test get_userinfo method with an invalid JWT response that
causes verify_token to raise an error.
"""
settings.OIDC_RP_SIGN_ALGO = "HS256" # disable JWKS URL call
responses.add(
responses.GET, re.compile(r".*/userinfo"), body="fake.jwt.token", status=200
responses.GET,
re.compile(r".*/userinfo"),
body="fake.jwt.token",
status=200,
content_type="application/jwt",
)
oidc_backend = OIDCAuthenticationBackend()
with pytest.raises(
SuspiciousOperation,
match="Invalid response format or token verification failed",
match="User info response was not valid JWT",
):
oidc_backend.get_userinfo("fake_access_token", None, None)
@@ -341,7 +432,7 @@ def test_authentication_getter_existing_disabled_user_via_email(
django_assert_num_queries, monkeypatch
):
"""
If an existing user does not matches the sub but matches the email and is disabled,
If an existing user does not match the sub but matches the email and is disabled,
an error should be raised and a user should not be created.
"""
@@ -365,3 +456,56 @@ def test_authentication_getter_existing_disabled_user_via_email(
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
assert models.User.objects.count() == 1
@responses.activate
def test_authentication_session_tokens(
django_assert_num_queries, monkeypatch, rf, settings
):
"""
Test that the session contains oidc_refresh_token and oidc_access_token after authentication.
"""
settings.OIDC_OP_TOKEN_ENDPOINT = "http://oidc.endpoint.test/token"
settings.OIDC_OP_USER_ENDPOINT = "http://oidc.endpoint.test/userinfo"
settings.OIDC_OP_JWKS_ENDPOINT = "http://oidc.endpoint.test/jwks"
settings.OIDC_STORE_ACCESS_TOKEN = True
settings.OIDC_STORE_REFRESH_TOKEN = True
settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key()
klass = OIDCAuthenticationBackend()
request = rf.get("/some-url", {"state": "test-state", "code": "test-code"})
request.session = {}
def verify_token_mocked(*args, **kwargs):
return {"sub": "123", "email": "test@example.com"}
monkeypatch.setattr(OIDCAuthenticationBackend, "verify_token", verify_token_mocked)
responses.add(
responses.POST,
re.compile(settings.OIDC_OP_TOKEN_ENDPOINT),
json={
"access_token": "test-access-token",
"refresh_token": "test-refresh-token",
},
status=200,
)
responses.add(
responses.GET,
re.compile(settings.OIDC_OP_USER_ENDPOINT),
json={"sub": "123", "email": "test@example.com"},
status=200,
)
with django_assert_num_queries(6):
user = klass.authenticate(
request,
code="test-code",
nonce="test-nonce",
code_verifier="test-code-verifier",
)
assert user is not None
assert request.session["oidc_access_token"] == "test-access-token"
assert get_oidc_refresh_token(request.session) == "test-refresh-token"

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,8 +59,32 @@ def test_api_document_accesses_list_authenticated_unrelated():
}
def test_api_document_accesses_list_unexisting_document():
"""
Listing document accesses for an unexisting document should return an empty list.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
response = client.get(f"/api/v1.0/documents/{uuid4()!s}/accesses/")
assert response.status_code == 200
assert response.json() == {
"count": 0,
"next": None,
"previous": None,
"results": [],
}
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
@pytest.mark.parametrize(
"role", [role for role in models.RoleChoices if role not in models.PRIVILEGED_ROLES]
)
def test_api_document_accesses_list_authenticated_related_non_privileged(
via, role, mock_user_teams
):
"""
Authenticated users should be able to list document accesses for a document
to which they are directly related, whatever their role in the document.
@@ -70,24 +94,114 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
owner = factories.UserFactory()
accesses = []
document_access = factories.UserDocumentAccessFactory(
user=owner, role=models.RoleChoices.OWNER
)
accesses.append(document_access)
document = document_access.document
if via == USER:
models.DocumentAccess.objects.create(
document=document,
user=user,
role=role,
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
models.DocumentAccess.objects.create(
document=document,
team="lasuite",
role=role,
)
access1 = factories.TeamDocumentAccessFactory(document=document)
access2 = factories.UserDocumentAccessFactory(document=document)
accesses.append(access1)
accesses.append(access2)
# Accesses for other documents to which the user is related should not be listed either
other_access = factories.UserDocumentAccessFactory(user=user)
factories.UserDocumentAccessFactory(document=other_access.document)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/accesses/",
)
# Return only owners
owners_accesses = [
access for access in accesses if access.role in models.PRIVILEGED_ROLES
]
assert response.status_code == 200
content = response.json()
assert content["count"] == len(owners_accesses)
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
[
{
"id": str(access.id),
"user": {
"id": None,
"email": None,
"full_name": access.user.full_name,
"short_name": access.user.short_name,
}
if access.user
else None,
"team": access.team,
"role": access.role,
"abilities": access.get_abilities(user),
}
for access in owners_accesses
],
key=lambda x: x["id"],
)
for access in content["results"]:
assert access["role"] in models.PRIVILEGED_ROLES
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize("role", models.PRIVILEGED_ROLES)
def test_api_document_accesses_list_authenticated_related_privileged_roles(
via, role, mock_user_teams
):
"""
Authenticated users should be able to list document accesses for a document
to which they are directly related, whatever their role in the document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
owner = factories.UserFactory()
accesses = []
document_access = factories.UserDocumentAccessFactory(
user=owner, role=models.RoleChoices.OWNER
)
accesses.append(document_access)
document = document_access.document
user_access = None
if via == USER:
user_access = models.DocumentAccess.objects.create(
document=document,
user=user,
role=random.choice(models.RoleChoices.choices)[0],
role=role,
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
user_access = models.DocumentAccess.objects.create(
document=document,
team="lasuite",
role=random.choice(models.RoleChoices.choices)[0],
role=role,
)
access1 = factories.TeamDocumentAccessFactory(document=document)
access2 = factories.UserDocumentAccessFactory(document=document)
accesses.append(access1)
accesses.append(access2)
# Accesses for other documents to which the user is related should not be listed either
other_access = factories.UserDocumentAccessFactory(user=user)
@@ -102,7 +216,7 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
assert response.status_code == 200
content = response.json()
assert len(content["results"]) == 3
assert len(content["results"]) == 4
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
[
{
@@ -126,6 +240,13 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_teams):
"role": access2.role,
"abilities": access2.get_abilities(user),
},
{
"id": str(document_access.id),
"user": serializers.UserSerializer(instance=owner).data,
"team": "",
"role": models.RoleChoices.OWNER,
"abilities": document_access.get_abilities(user),
},
],
key=lambda x: x["id"],
)
@@ -184,7 +305,10 @@ def test_api_document_accesses_retrieve_authenticated_unrelated():
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_teams):
@pytest.mark.parametrize("role", models.RoleChoices)
def test_api_document_accesses_retrieve_authenticated_related(
via, role, mock_user_teams
):
"""
A user who is related to a document should be allowed to retrieve the
associated document user accesses.
@@ -196,10 +320,12 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_tea
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user)
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
access = factories.UserDocumentAccessFactory(document=document)
@@ -207,16 +333,19 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_tea
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
)
access_user = serializers.UserSerializer(instance=access.user).data
if not role in models.PRIVILEGED_ROLES:
assert response.status_code == 403
else:
access_user = serializers.UserSerializer(instance=access.user).data
assert response.status_code == 200
assert response.json() == {
"id": str(access.id),
"user": access_user,
"team": "",
"role": access.role,
"abilities": access.get_abilities(user),
}
assert response.status_code == 200
assert response.json() == {
"id": str(access.id),
"user": access_user,
"team": "",
"role": access.role,
"abilities": access.get_abilities(user),
}
def test_api_document_accesses_update_anonymous():
@@ -227,7 +356,7 @@ def test_api_document_accesses_update_anonymous():
new_values = {
"id": uuid4(),
"user": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.choices)[0],
"role": random.choice(models.RoleChoices.values),
}
api_client = APIClient()
@@ -260,7 +389,7 @@ def test_api_document_accesses_update_authenticated_unrelated():
new_values = {
"id": uuid4(),
"user": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.choices)[0],
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():
@@ -302,7 +431,7 @@ def test_api_document_accesses_update_authenticated_reader_or_editor(
new_values = {
"id": uuid4(),
"user": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.choices)[0],
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():
@@ -413,7 +542,7 @@ def test_api_document_accesses_update_administrator_from_owner(via, mock_user_te
new_values = {
"id": uuid4(),
"user_id": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.choices)[0],
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():
@@ -527,7 +656,7 @@ def test_api_document_accesses_update_owner(
new_values = {
"id": uuid4(),
"user_id": factories.UserFactory().id,
"role": random.choice(models.RoleChoices.choices)[0],
"role": random.choice(models.RoleChoices.values),
}
for field, value in new_values.items():

View File

@@ -16,6 +16,9 @@ from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
# Create
def test_api_document_accesses_create_anonymous():
"""Anonymous users should not be allowed to create document accesses."""
document = factories.DocumentFactory()
@@ -26,7 +29,7 @@ def test_api_document_accesses_create_anonymous():
{
"user_id": str(other_user.id),
"document": str(document.id),
"role": random.choice(models.RoleChoices.choices)[0],
"role": random.choice(models.RoleChoices.values),
},
format="json",
)
@@ -123,7 +126,7 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
document=document, team="lasuite", role="administrator"
)
other_user = factories.UserFactory()
other_user = factories.UserFactory(language="en-us")
# It should not be allowed to create an owner access
response = client.post(
@@ -199,7 +202,7 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
document=document, team="lasuite", role="owner"
)
other_user = factories.UserFactory()
other_user = factories.UserFactory(language="en-us")
role = random.choice([role[0] for role in models.RoleChoices.choices])
@@ -235,3 +238,73 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
f"on the following document: {document.title}"
) in email_content
assert "docs/" + str(document.id) + "/" in email_content
@pytest.mark.parametrize("via", VIA)
def test_api_document_accesses_create_email_in_receivers_language(via, mock_user_teams):
"""
The email sent to the accesses to notify them of the adding, should be in their language.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="owner"
)
role = random.choice([role[0] for role in models.RoleChoices.choices])
assert len(mail.outbox) == 0
other_users = (
factories.UserFactory(language="en-us"),
factories.UserFactory(language="fr-fr"),
)
for index, other_user in enumerate(other_users):
expected_language = other_user.language
response = client.post(
f"/api/v1.0/documents/{document.id!s}/accesses/",
{
"user_id": str(other_user.id),
"role": role,
},
format="json",
)
assert response.status_code == 201
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
new_document_access = models.DocumentAccess.objects.filter(
user=other_user
).get()
other_user_data = serializers.UserSerializer(instance=other_user).data
assert response.json() == {
"id": str(new_document_access.id),
"user": other_user_data,
"team": "",
"role": role,
"abilities": new_document_access.get_abilities(user),
}
assert len(mail.outbox) == index + 1
email = mail.outbox[index]
assert email.to == [other_user_data["email"]]
email_content = " ".join(email.body.split())
email_subject = " ".join(email.subject.split())
if expected_language == "en-us":
assert (
f"{user.full_name} shared a document with you: {document.title}".lower()
in email_subject.lower()
)
elif expected_language == "fr-fr":
assert (
f"{user.full_name} a partagé un document avec vous : {document.title}".lower()
in email_subject.lower()
)
assert "docs/" + str(document.id) + "/" in email_content.lower()

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