Compare commits

..

111 Commits

Author SHA1 Message Date
Manuel Raynaud
441ad03988 🔧(helm) publish version 4.8.0-psycopgpool 2026-03-16 22:22:27 +01:00
Manuel Raynaud
04c9dc3294 🔧(backend) allow to configure psycopg pool timeout
We want to allow the configuration of the psycopg pool timeout.
For this we created a new setting DB_PSYCOPG_POOL_TIMEOUT
2026-03-16 15:30:23 +01:00
Manuel Raynaud
32b2641fd8 (hub) increase max pool size
In order to run the tests we need to increase the max pool size. Only
having 4 connections in the pool is not enough and all the tests using a
transaction are failing with a tiemout error.
We have the same problem running locally so the same value is added to
the postgresql environment file
2026-03-16 15:30:23 +01:00
Manuel Raynaud
07966c5461 🔧(helm) update values.yaml annotations
The annotation in the values.yaml have not been updated since a while.
This commit update them and generate the readme using the generate-readme.sh
script
2026-03-16 15:30:23 +01:00
Manuel Raynaud
bcb50a5fce 🔧(helm) allow specific env var for the backend and celery deploy
We want the possibility to configure specific environment variables on
backend and celery deployment. Most of them are common but in the case
of the newly added settings DB_PSYCOPG_POOL_MIN_SIZE we want to
configure ot only on the backend deployment, not on the celery or with a
different value.
2026-03-16 15:30:22 +01:00
Manuel Raynaud
ba93bcf20b 🔧(backend) enable psycopg-pool allowing configuring min and max size
We enable the pool option on the DB configuration. We want to allow the
configuration of the min and max sixe in a first time. They can be
configured using the settings DB_PSYCOPG_POOL_MIN_SIZE and
DB_PSYCOPG_POOL_MAX_SIZE. They have their default value to 4 and None.
2026-03-16 15:30:22 +01:00
Manuel Raynaud
2e05aec303 (backend) install psycopg_pool
We want to use psycopg_pool, it can be installed as a psycopg extra
dependency.
2026-03-16 15:30:22 +01:00
Anthony LC
51e8332b95 🔖(minor) release 4.8.0
Added:
- (backend) add a is_first_connection flag to the User model
- (frontend) add onboarding modal with help menu button

Changed:
- (frontend) localize LaGaufre label fallback in Docs
- (backend) add a migration cleaning on-boarding
  document accesses
- ⬆️(frontend) upgrade Next.js to v16
- ️(frontend) fix aria-label and landmark on document
  banner state
- 🌐(i18n) add "new window" translation key for waffle
  aria-label

Fixed:
- 🐛(backend) create a link_trace record for on-boarding
  documents
- 🐛(backend) manage race condition when creating sandbox
  document
- 🐛(frontend) fix flickering left panel
- ️(frontend) improve doc tree keyboard navigation
2026-03-13 18:00:32 +01:00
AntoLC
eb2ee1bb7f 🌐(i18n) update translated strings
Update translated files with new translations
2026-03-13 18:00:32 +01:00
Anthony LC
d34f279455 📱(frontend) improve mobile design left panel
Improve the onboarding modal design for
mobile devices.
Improve as well the left panel on mobile devices
to fit more with the Figma design.
2026-03-13 17:22:55 +01:00
Anthony LC
3eed542800 (frontend) display onboarding modal when first connection
When the user connect for the first time, we
display a onboarding modal, that explains the
main functionnalities of Docs.
2026-03-13 17:22:54 +01:00
Anthony LC
5f2c472726 🌐(frontend) add currentLocale to CunninghamProvider
In order to have the text of components from the
Cunningham library translated, we need to pass the current
locale to the CunninghamProvider.
We need to create a new ThemeProvider component that
will wrap the CunninghamProvider in order to have
react-query fully loaded.
2026-03-13 17:22:54 +01:00
Cyril
9e313e30a7 (frontend) add e2e test for onboarding modal
Ensure onboarding entrypoint and modal navigation work end-to-end.
2026-03-13 17:22:54 +01:00
Cyril
6c493c24d5 (frontend) add onboarding modal with help menu button
integrate onboarding feature accessible from left panel help menu

(frontend) add docs onboarding and help memu

Introduce an onboarding to guide users through core features.
2026-03-13 16:27:21 +01:00
Anthony LC
c3acfe45d2 🐛(frontend) fix skeleton blocked on main page
If navigating quickly between documents, the
skeleton of the document page can be blocked
on the main page.
This commit fixes this issue by reseting the skeleton
state when unmounting the document page.
2026-03-13 11:10:13 +01:00
Anthony LC
a9d2517c7b 🐛(frontend) fix flickering left panel
In some cases, the left panel can flicker
when navigating from the index to a document page.
This is due to different state + a transition effect.
To fix this, we remove the transition effect
when mounting.
2026-03-13 10:34:56 +01:00
Cyril
a2ae41296d ️(frontend) fix doc tree keyboard navigation regressions
Shift+Tab from sub-doc returns focus to root item
2026-03-12 17:10:09 +01:00
Cyril
1016b1c25d ️(i18n) add "new window" translation key for waffle aria-label
Add key used by LaGaufreV2 for localized aria-label on external links.
2026-03-12 16:13:51 +01:00
Cyril
0c649a65b0 ️(frontend) fix redundant a-label and improper landmark on public alert
Remove aria-label and region role to avoid duplicate screen reader announcement
2026-03-12 15:10:25 +01:00
Anthony LC
11d899437a ️(frontend) improve bundle size
Improve bundle size by improving tree shaking
and code splitting.
2026-03-12 14:34:23 +01:00
Anthony LC
27c5e0ce5a (frontend) use eslint instead of next lint
Version 16 of Next.js stopped supporting eslint natively.
We need to implement it ourselves.
2026-03-12 14:34:23 +01:00
Anthony LC
9337c4b1d5 ♻️(frontend) adapt emoji copying to turbopack build
We were previously copying the emoji assets
in a webpack plugin, but that doesn't run with
turbopack. This commit moves the copying to a
pre-build script, which runs regardless of the
bundler used.
2026-03-12 14:34:23 +01:00
Anthony LC
679b29e2e0 ⬆️(frontend) upgrade Next.js to v16
Upgrade Next.js to v16, which includes Turbopack
support by default. It improves dev and build
performance considerably.
2026-03-12 14:34:23 +01:00
Manuel Raynaud
3cad1b8a39 (backend) add a migration for cleaning onboarding document accesses
We change the strategy on how the new users have access to the
onboarding documents. We should remove all created accesses we don't
want to have anymore. There is no need to add them in the link_trace
table, they are already present in the favorites and user have already
access to it.
2026-03-12 13:52:23 +01:00
Manuel Raynaud
2eb2641d2c 🐛(backend) manage race condition when creating sandbox document
When a user is created and a sandbox document should be created, we can
have a race condition on the document creation leading to an error for
the user. To avoid this we have to manage this part in a transaction and
locking the document table
2026-03-12 13:51:41 +01:00
Manuel Raynaud
e36366b293 🐛(backend) create a link_trace record for onboarded documents
When a user is created, we created accesses to a list of onboarding
documents. Doing this have side effect on the proximity search feature.
Instead of creating access, we should create link_reach
2026-03-12 13:51:41 +01:00
Cyril
6d73fb69b0 ️(frontend) localize LaGaufre label fallback in Docs
We pass a translated fallback label so the waffle follows the app locale.
2026-03-12 11:07:24 +01:00
Sylvain Boissel
b708c8b352 (backend) add a is_first_connection flag to the User model
Backend part of #1796.

This changes allows to display an onboarding modal the first time that
the get_me() API view is called.
I originally tried to check if `User.last_login` was `None`, but it is
updated as soon as the user is logged, so I chose to create a flag on
the model.
2026-03-11 14:34:55 +00:00
Manuel Raynaud
36c6762026 ⬇️(backend) downgrade langfuse to version 3.11.2
We to keep in sync the version of the sdk client and the version of the
langfuse server. For now we can't upgrade langfuse See
https://github.com/langfuse/langfuse/issues/11564
2026-03-11 09:54:26 +00:00
Hadrien Blanc
4637d6f1fe 📝 Fix documentation and comment typos (#1977)
Fix typos found in documentation and code comments across the codebase.
2026-03-11 09:29:57 +00:00
Anthony LC
167375231b 🔖(minor) release 4.7.0
Added:
- (helm) allow all keys in configMap as env var

Changed:
- 📝(docs) improve README and add documentation hub
- ️(frontend) restore focus to triggers after closing menus and modals
- 🚸(frontend) change position elements toolbar
- ️(frontend) add focus on open to modals

Fixed:
- 🐛(frontend) analytic feature flags problem
- 🐛(frontend) fix home collapsing panel
- 🐛(frontend) fix disabled color on icon Dropdown
- 🐛(frontend) fix zIndex table of content
- 🐛(frontend) fix bug when language not supported by BN
- 🐛 (backend) prevent privileged users from requesting access
2026-03-10 10:25:11 +01:00
Anthony LC
c17fb3e6cc 🩹(frontend) fix small ui issue
- Better position of close icon modal
- Fix some z-index issues with the floating bar
- Fix translation not made
2026-03-10 10:25:11 +01:00
AntoLC
1be89180fe 🌐(i18n) update translated strings
Update translated files with new translations
2026-03-09 18:27:30 +01:00
Manuel Raynaud
6a3b33ec32 ⬇️(backend) downgrade pydantic-ai-slim to version 1.58.0
We upgrade pydantic-ai-slim to version 1.63.0, we have an error when
Docs is run in preprod environment.
We have to investigate why and we decided to downgrade to a working
version
2026-03-09 18:27:30 +01:00
Anthony LC
29f2c2ebdf 💄(frontend) fix flickering tree title
The title was flickering because the icons were
loading and pushing the div to the left. We do not
need the icons part if we are not hovering the tree
item, so we can hide it until we hover the item.
2026-03-09 14:43:38 +01:00
Anthony LC
9d320092df ⬆️(frontend) bump blocknote to 0.47.1
Bump BlockNote to 0.47.1 to fix the issues with
the ai selections.
2026-03-09 14:43:38 +01:00
Anthony LC
77535b0292 🚸(frontend) change position elements toolbar
The AI and comment toolbar buttons are now
positioned at the end of the toolbar.
They were taking too much space on the left
which was not ideal for user experience.
2026-03-09 14:43:38 +01:00
Anthony LC
770c22b1a6 🐛(frontend) fix bug when language not supported by BN
When a language was not supported by BlockNote,
the editor would crash. This commit adds a default
locale fallback to prevent the crash and ensure
the editor remains functional even when the
user's language is not supported.
2026-03-09 14:43:37 +01:00
Anthony LC
3c980512be 🐛(frontend) improve position ai menu elements
The right part of the ai menu was not correctly
positioned, this commit fixes this issue.
2026-03-09 14:43:37 +01:00
Anthony LC
76cb6d66a4 🐛(frontend) fix panel collapse handle
When handling to the left, the panel could totally
collapse, and do beeing unsync with the state of
the left panel (open or closed).
We now keep the collapsing deactivate until
the state of the left panel is closing.
2026-03-09 14:03:57 +01:00
Anthony LC
6cef5ff2a0 🐛(frontend) fix buttons closing panel
When clicking on the home button, the left panel was
collapsing, it is a behavior that we want only when
mobile. We improved the logic to only collapse the
panel when we are on mobile devices.
2026-03-09 14:03:57 +01:00
Manuel Raynaud
d816234839 ♻️(backend) change treebeard NumConv useage
In version 5 of django-treebeard they change the usage of NumConv for
something easier. They also introduce a breaking change, we have to
change the usage of this class in the migration 15.
2026-03-09 12:10:53 +01:00
renovate[bot]
5dd66f0cdc ⬆️(dependencies) update python dependencies 2026-03-09 12:10:52 +01:00
dtinth-ampere-devbox
0a4052d023 ♻️(frontend) replace hardcoded colors with contextual tokens
Replaced hardcoded global color tokens
(gray-000, gray-100, gray-600, white, gainsboro)
with Cunningham contextual tokens to enable proper
dark mode support.

Components updated:
- LeftPanel: background colors
- DropButton: popover background
- DropdownMenu: menu item background
- DocEditor: editor container background
- DocEditor styles: BlockNote CSS variables,
  inline code, links
- TableContent: panel and heading backgrounds

This allows the UI to automatically adapt between
light and dark themes.

Signed-off-by: dtinth-ampere-devbox <dtinth@spacet.me>
Signed-off-by: dtinth on MBP M1 <dtinth@spacet.me>
2026-03-09 11:21:40 +01:00
Anthony LC
189594c839 🐛(frontend) fix zIndex table of content
When we open a modal, the table of content is
above the modal, which is not the expected behavior.
This commit fixes the zIndex of the table of
content to be below the modal.
2026-03-09 10:31:47 +01:00
Mohamed El Amine BOUKERFA
ca286b6de7 🐛(backend) prevent privileged users from requesting access
Privileged users of a document (owners and admins) should not be
allowed to create an access request on a document they already control.
Without aguardrail, they could inadvertently inflate the access
request queue with redundant entries.

More critically, if an owner submits an access request on their own
document, another admin could accept it and grant them a lower role
(e.g. reader), which would silently strip them of their ownership.

Signed-off-by: Mohamed El Amine BOUKERFA <boukerfa.ma@gmail.com>
2026-03-09 10:07:23 +01:00
Anthony LC
6062d0e9c4 🐛(frontend) fix disabled color on icon Dropdown
When the item was disabled, the icon color was not
updated to the disabled color. This commit fixes
this issue by applying the disabled color
to the icon when the item is disabled.
2026-03-06 15:06:00 +01:00
Anthony LC
a51b34a04e 🐛(frontend) analytic feature flags problem
When multiple analytics are registered, if one
analytic has the flag at true, the feature was activated,
even if another analytic had it at false.
We change the logic to require all analytics to
have the flag at true for the feature to be activated.
2026-03-06 14:40:14 +01:00
Johannes Kastl
f294a8e5a3 (helm) allow all keys in configMap as env var in backend jobs
Add `envFrom` support to backend jobs and cronjobs
(`backend_job.yml`, `backend_job_createsuperuser.yaml`,
`backend_job_migrate.yaml`, `backend_cronjob_list.yaml`),
mirroring the `envFrom` support already added to the deployments.

This allows injecting all keys from a ConfigMap or Secret as
environment variables via `backend.envFrom` in the Helm values.

Signed-off-by: Johannes Kastl <git@johannes-kastl.de>
2026-03-06 12:31:54 +01:00
Johannes Kastl
b4591cda10 (helm) allow keys in configMap or secret as env var
The current envVars construct only allows single
keys in a configMap or secret to be used as
environment variables.
This commits keeps this functionality for
backwards compatibility, but adds another
envFrom variable that allows using all keys in a
secret or configMap as environment variables.

Signed-off-by: Johannes Kastl <git@johannes-kastl.de>
2026-03-06 12:29:01 +01:00
Johannes Kastl
301bf43cb7 🐛(helm) only use env: if envVars is defined
Previously, the `env:` key was always rendered in
Kubernetes manifests even when no environment
variables were defined, resulting in an empty
`env:` block. This wraps the entire `env:`
section in a conditional so it is only included
when `envVars` is actually set.

Signed-off-by: Johannes Kastl <git@johannes-kastl.de>
2026-03-06 12:24:12 +01:00
Johannes Kastl
f155e9217e (helm) allow mounting secrets via extraVolumes
The chart already allows mounting configMaps. This commits adds
the functionality to allow mounting secrets.

Signed-off-by: Johannes Kastl <git@johannes-kastl.de>
2026-03-06 11:32:52 +01:00
Cyril
09fb9671e4 ️(frontend) restore focus on docIcon after picker
Use focus store to return focus after emoji picker close.
2026-03-06 10:45:01 +01:00
Cyril
4c0c1f423e ️(frontend) restore focus in docToolBox after modals
Use focus store to return focus to toolbar buttons.
2026-03-06 10:44:55 +01:00
Cyril
83fe903587 ️(frontend) restore focus in docsGrid after modals
Use focus store to restore focus to triggers in docsGrid.
2026-03-06 10:44:49 +01:00
Cyril
200b975c6d ️(frontend) restore focus in docTree after actions
Improve F2 shortcut handling in DocTree and DocSubPageItem.
2026-03-06 10:44:42 +01:00
Cyril
9536227c52 ️(frontend) add focus restore store
Create Zustand store to track last focused element
and restore focus after overlay close. DropdownMenu
auto-stores trigger ref when an option is clicked.
2026-03-06 10:44:27 +01:00
networkException
fb4c502c75 🐛(backend) properly encode attachment upload content-disposition header
This patch changes the Content-Disposition header set on S3 PutObject
requests when uploading attachments to be encoded using a Django helper,
properly encoding UTF-8 in filenames.

This fixes compatibility for uploading attachments with non ASCII file names
to Garage, since the HTTP library used by Garage validates that HTTP Headers
are ASCII only.

Signed-off-by: networkException <git@nwex.de>
2026-03-06 10:13:24 +01:00
Stephan Meijer
77aee5652a ⬆️(ci) upgrade GitHub Actions workflow steps to latest versions
Update all GitHub Actions to their latest major versions for improved
performance, security patches, and Node.js runtime compatibility.

Signed-off-by: Stephan Meijer <me@stephanmeijer.com>
2026-03-06 09:51:18 +01:00
Manuel Raynaud
7cceffff13 🔥(backend) remove rust from the back-builder Dockerfile stage
Rust was installed to build the pycrdt wheel. There is no need anymore
to install rust, a wheel is available for this package.
2026-03-05 21:38:05 +01:00
Manuel Raynaud
a028df54ce ⬆️(backend) upgrade pycrdt to version 0.12.47
This version has a wheel for musl linux platform, it should speed up the
build of the docker images
2026-03-05 21:38:05 +01:00
Cyril
25cf11c90f ️(frontend) add focus on open to modals
Use autoFocus on primary button when opening modals.

Made-with: Cursor
2026-03-05 17:39:02 +01:00
pav
d1a3519646 📝(docs) improve readme and add documentation hub (#1870)
## Purpose

This pull request improves the project’s documentation entry points and
overall readability to make Docs more approachable for new users and
contributors.

While reviewing the repository, I noticed that the project highlights
documentation and Markdown support, but the front-page README contained
several Markdown syntax issues and inconsistencies. This made the
landing experience feel less polished than the quality of the project
itself. The goal of this change is to provide a cleaner, more
consistent, and more professional first impression.

Please let me know and I can apply any changes, or edit other .md files
as needed!

## Proposal

- Rewrite the root README to be tighter, easier to scan, and more
user-facing
- Add a documentation landing page at `/docs/README.md` with a
structured table of contents
- Introduce `docs/instances.md` to list public Docs instances  

## External contributions

Thank you for your contribution! 🎉  

Please ensure the following items are checked before submitting your
pull request:

- [x] I have read and followed the [contributing
guidelines](https://github.com/suitenumerique/docs/blob/main/CONTRIBUTING.md)
- [x] I have read and agreed to the [Code of
Conduct](https://github.com/suitenumerique/docs/blob/main/CODE_OF_CONDUCT.md)
- [x] I have signed off my commits with `git commit --signoff` (DCO
compliance)
- [x] I have signed my commits with my SSH or GPG key (`git commit -S`)
- [x] My commit messages follow the required format: `<gitmoji>(type)
title description`
- [x] I have added a changelog entry under `## [Unreleased]` section  
- [x] I have not added tests because this PR only contains documentation
changes

---------

Signed-off-by: actuallypav <61046893+actuallypav@users.noreply.github.com>
2026-03-05 14:55:35 +00:00
Anthony LC
03ea6b29df 🔖(minor) release 4.6.0
Added:
- (frontend) integrate new Blocknote AI feature
- 👷(docker) add arm64 platform support for image builds
- (tracking) add UTM parameters to shared document links
- (frontend) add floating bar with leftpanel collapse button
- (frontend) Can print a doc
- (backend) manage reconciliation requests for user accounts
- 👷(CI) add GHCR workflow for forked repo testing
- (frontend) Move doc modal
- ️(backend) remove content from Document serializer when asked
- (backend) allow the duplication of subpages
- (backend) Onboarding docs for new users
- 🩺(trivy) add trivyignore file and add minimatch CVE
- 🚩 Add feature flags for the AI feature

Changed:
- ️(frontend) prevent dates from being focusable
- ️(frontend) Focus main container after navigation
- 💄(frontend) align colors and logo with ui-kit v2
- 🚸(backend) sort user search results by proximity
  with the active user
- 🚸(oidc) ignore case when fallback on email
- ️(CI) optimize Docker Hub workflow

Fixed:
- 🐛(frontend) fix broadcast store sync
- 🐛(helm) use celery resources instead of backend resources
- 🐛(helm) reverse liveness and readiness for backend deployment
- 🐛(y-provider) use CONVERSION_FILE_MAX_SIZE settings
- 🐛(frontend) fix callout block spacing for old browsers
2026-03-05 12:42:03 +01:00
Anthony LC
ea0a1aef10 (e2e) fix flaky test on doc move
Some tests on the doc move functionality
were flaky, blocking the workflow.
2026-03-05 12:42:03 +01:00
AntoLC
bb7d1353f6 🌐(i18n) update translated strings
Update translated files with new translations
2026-03-05 09:30:41 +01:00
Anthony LC
1944f6177e ♻️(frontend) Add default data to move modal
By default the move modal has the last updated doc
in the list, when typing it starts filtering.
In order to have this behaviour possible, we had to
refactorise DocSearchContent and the QuickSearch
components to be able to set a default value in
the input and a default list of items.
2026-03-04 16:00:59 +01:00
Anthony LC
6ce847d6e1 ♻️(frontend) change the closing buttons of the modals
We changed the closing buttons
of the modals to be consistent with the new design
system. We adapted the modals to fit the new design
and ensure a better user experience.
2026-03-04 16:00:59 +01:00
Anthony LC
e48080b27e ♻️(frontend) refact HorizontalSeparator to be more flexible
We need HorizontalSeparator to be more flexible,
so we can use it in more places. We change the
props to be more generic, we updated the components
that use it.
2026-03-04 16:00:59 +01:00
Anthony LC
73621c91e5 🍱(frontend) add icons ui-kit
We have a mix of Material icons and custom icons
in the app, which makes it difficult to maintain
and update them. To solve this problem, we downloaded
all the icons from the ui-kit and added them to
the project. We will gradually replace the old
icons with the new ones to ensure a consistent
and up-to-date icon set across the app.
2026-03-04 16:00:59 +01:00
Anthony LC
ee2462310f ♻️(frontend) Update base role for access request
The base role for access request is now "editor"
instead of "viewer".
2026-03-03 15:31:03 +01:00
Manuel Raynaud
2d6e34c555 ♻️(ci) reuse amd64 to build arm64 image when possible
Building twice the image take lof of time. In soma cases, building the
arm64 image using the artifacts build in the amd64 and thant can be
reused should speed up the build of the arm64 image.
2026-03-03 14:48:13 +01:00
Manuel Raynaud
3f638b22c4 ♻️(ci) extract docker build job in a dedicated job to custom it
We extract the docker build and push job to a dedicated action called
to make the docker job for each application. This action avoid
copy/paste between each job.
2026-03-03 14:48:13 +01:00
Anthony LC
c9f42e7924 🩹(frontend) improve minor ux/ui
- Better logo resolution (x4)
- Remove floating bar when printing a doc
- Add loader during doc import
2026-03-02 11:49:21 +01:00
Anthony LC
a30384573e 🔇(frontend) remove ads console i18next
i18next was adding ads to the console. We remove it from
the console thanks to "showSupportNotice" option.
We upgraded to have less noisy console as well
during development.
2026-03-02 11:12:49 +01:00
Anthony LC
54dc72209c 🩺(trivy) add minimatch CVE to trivyignore
We added CVE-2026-27903 and CVE-2026-27904 to
the .trivyignore file
We need to wait for the community to update the
docker image with a fix for these CVEs before we
can remove them from the .trivyignore file.
2026-02-27 09:58:57 +01:00
Anthony LC
9cf30a0d5f 🔒️(secu) fix CVE warning
Fix CVE warning about:
- CVE-2026-27606 about rollup
- CVE-2026-26996 about minimatch
2026-02-27 09:55:36 +01:00
Anthony LC
f24b047a7c 🛂(frontend) add access request modal on move modal
If a user tries to move a document for which they
don't have the right to move, we now display a
modal to request access to the owners of the
document.
2026-02-27 09:13:29 +01:00
Anthony LC
3411df09ae 🛂(frontend) add confirmation modal on move modal
If the document has more than 1 direct access,we want to
display a confirmation modal before moving the document.
This is to prevent users from accidentally moving a document
that is shared with multiple people.
The accesses and invitations will be removed from the
document.
2026-02-27 09:12:04 +01:00
Anthony LC
2718321fbe (frontend) Move doc modal
We can now move a doc to another doc from a search
modal. It will make it easier to move a doc
without having to scroll through the doc grid to
find the destination doc.
We kept most of the logic implemented in the
doc grid dnd.
2026-02-27 09:09:25 +01:00
Anthony LC
217af2e2a8 🐛(frontend) disable DND grid item when dialog open
We could drag and drop the items even if the
modal was opened, which could cause some unexpected
behaviors. This commit disables the DND
functionality when a dialog box is open.
2026-02-27 09:08:51 +01:00
Anthony LC
53985f77f3 🚚(frontend) move modal Share directly to concern component
To keep consistency with the other modals, we move
the share modal directly to the
DocsGridActions component. This way, we avoid
having to pass down the openShareModal function
from parent components and keep the logic related
to sharing a doc encapsulated within the
concern component.
2026-02-27 09:08:51 +01:00
Anthony LC
a51ceeb409 🚩(frontend) add Analytics feature flag on Blocknote AI
We want to have a fine grained control over the
Blocknote AI feature.
By adding the feature Analytics feature flag,
we can enable or disable this feature for specific
users or groups without deploying new code.
This allows us to test the feature in a controlled
environment and gather feedback before a full rollout.
2026-02-26 13:50:17 +01:00
Anthony LC
1070b91d2f 🚩(project) add more backend AI feature flags
The Blocknote AI feature is a bit flaky, we want
to be able to disable it if to much issues arise,
without having to do a new release.
We add a bunch of feature flags to be able to
disable the AI features if needed:
- add AI_FEATURE_BLOCKNOTE_ENABLED, to display
or not the feature powered by blocknote
- add AI_FEATURE_LEGACY_ENABLED, to display or not
the legacy AI features
2026-02-26 13:50:17 +01:00
Manuel Raynaud
24ec1fa70e 🔥(backend) remove settings AI_STREAM
We don't need anymore the AI_STREAM settings, we use the stream all the
time.
2026-02-26 09:52:34 +01:00
Manuel Raynaud
0ba6f02d1a (backend) force usage of system prompt along when using tools
When the tool applyDocumentOperations is used, we have to force the
usage of a system prompt in order to force the model to use it the right
without inventing different actions. The pydantic Agent class can use a
system prompt but this noe is ignoried when a UI adapter is used like
the VercelAiAdapter.
2026-02-26 09:52:34 +01:00
Manuel Raynaud
8ce216f6e8 (backend) use pydantic AI to manage vercel data stream protocol
The frontend application is using Vercel AI SDK and it's data stream
protocol. We decided to use the pydantic AI library to use it's vercel
ai adapter. It will make the payload validation, use AsyncIterator and
deal with vercel specification.
2026-02-26 09:52:33 +01:00
Manuel Raynaud
050b106a8f ️(asgi) use uvicorn to serve backend
This is a naive first switch from sync to async.
This enables the backend to still answer to incomming requests
while streaming LLM results to the user.

For sure there is room for code cleaning and improvements, but
this provides a nice improvement out of the box.
2026-02-26 09:52:33 +01:00
Cyril
5011db9bd7 ️(frontend) announce ai status with aria-live
notify screen readers about ai thinking, writing, ready, or error
2026-02-26 09:52:33 +01:00
Anthony LC
e1e0e5ebd8 🛂(frontend) bind ai_proxy abilities with AI feature
Bind ai_proxy abilities to the AI feature.
If ai_proxy is false, the AI feature will
not be available.
2026-02-26 09:52:33 +01:00
Anthony LC
5c8fff01a5 📄(frontend) remove AI feature when MIT
AI feature is under AGPL license, so it is removed
when the project is under MIT license.
NEXT_PUBLIC_PUBLISH_AS_MIT manage this.
2026-02-26 09:52:33 +01:00
Anthony LC
1a022450c6 (frontend) integrate new Blocknote AI feature
We integrate the new Blocknote AI feature
into Docs, enhancing the document editing experience
with AI capabilities.
2026-02-26 09:52:33 +01:00
Anthony LC
09438a8941 🛂(backend) harden payload proxy ai
Standard can vary depending on the AI service used.
To work with Albert API:
- a description field is required in the payload
  for every tools call.
- if stream is set to false, stream_options must
  be omitted from the payload.
- the response from Albert sometimes didn't respect
  the format expected by Blocknote, so we added a
  system prompt to enforce it.
2026-02-26 09:48:03 +01:00
Manuel Raynaud
6f0dac4f48 (back) manage streaming with the ai service
We want to handle both streaming or not when interacting with the AI
backend service.
2026-02-26 09:48:03 +01:00
Anthony LC
9d6fe5da8f 🔧(backend) make frontend ai bot configurable
We make the AI bot configurable with settings.
We will be able to have different AI bot name
per instance.
2026-02-26 09:48:03 +01:00
Anthony LC
1ee313efb1 (backend) add ai_proxy
Add AI proxy to handle AI related requests
to the AI service.
2026-02-26 09:48:02 +01:00
Anthony LC
1ac6b42ae3 ️(frontend) fetch document without content when not needed
To improve performance, especially for documents
with large content, an optional query parameter
`without_content` has been added to the document
fetching API endpoint. When this parameter is set
to true, the API will return the document
metadata without the content, allowing the frontend
to load faster and reduce unnecessary data transfer.
2026-02-25 21:18:01 +01:00
Manuel Raynaud
ffae927c93 ️(backend) remove content from Document serializer when asked
The frontend can fetch the retrieve endpoint just for having the title.
Everytime, the content is added and to get the content, a request is
made to the s3 bucket. A query string `without_content` can be used, if
the value is `true` then the content is not added (and not also not
fetch).
2026-02-25 21:18:01 +01:00
Anthony LC
0d335105a1 🔒️(secu) fix CVE warning
Fix CVE warning about:
- CVE-2026-22036 about undici
- CVE-2025-13465 about lodash
- CVE-2026-2327 about markdown-it
- CVE-2025-64756 about glob
- CVE-2026-2391 about qs
- CVE-2026-24001 about diff
2026-02-25 15:25:30 +01:00
Anthony LC
dc23883a9c ⬆️(frontend) update dependencies to fit eslint v10
Update dependencies and code to fit eslint v10.
2026-02-25 14:59:13 +01:00
renovate[bot]
a8ce9eabf8 ⬆️(dependencies) update js dependencies 2026-02-25 14:59:13 +01:00
Anthony LC
21217be587 ️(CI) optimize Docker Hub workflow
We use the Docker Hub Workflow to build and push
our images to Docker Hub, but to check if we
have vulnerabilities in our images as well.
When we are just checking for vulnerabilities,
we don't need to do all the builing steps.
This commit optimizes the workflow by only doing the
necessary steps when we are just checking for
vulnerabilities, so during pull requests
without label "preview" we skip the build steps,
and we do not activate QEMU.
2026-02-25 14:40:38 +01:00
Anthony LC
a8212753aa 🩺(trivy) add trivyignore file and add minimatch CVE
We added a .trivyignore file to ignore the
CVE-2026-26996 vulnerability coming from the y-provider
image.
We also updated the docker-hub.yml workflow to
use this .trivyignore file when scanning our
Docker images with Trivy.
2026-02-25 11:14:02 +01:00
Anthony LC
c37dc8dd34 🐛(frontend) fix callout block spacing for old browsers
On the old Firefox versions, the spacing of the
callout block was not applied correctly.
We changed the "white-space" property to "pre-wrap"
to ensure that the spacing is applied correctly on
all browsers.
2026-02-24 15:45:49 +01:00
Anthony LC
e323af2cdb 🐛(y-provider) use CONVERSION_FILE_MAX_SIZE settings
The settings CONVERSION_FILE_MAX_SIZE was not used
in the y-provider, which caused a 413 Payload
Too Large error when trying to convert a file larger
than 500kb.
This commit updates the y-provider to use the
CONVERSION_FILE_MAX_SIZE settings, allowing it to
handle larger files without throwing an error.
CONVERSION_FILE_MAX_SIZE should follow the same
value as the one defined in the backend settings,
which is 20mb by default.
2026-02-24 14:26:08 +01:00
Sylvain Boissel
9f9f26974c 🚸(backend) update missing doc management in onboarding sandbox feature
Update _duplicate_onboarding_sandbox_document() to return immediately
if the doc defined in settings can't be found.
2026-02-23 16:29:08 +01:00
Sylvain Boissel
c80e7d05bb 🚸(backend) add onboarding docs for new users
Adds two methods to allow new users to start with some docs.

User._handle_onboarding_documents_access() gives READER access to
each document listed in settings.USER_ONBOARDING_DOCUMENTS.

User._duplicate_onboarding_sandbox_document() creates a local copy
of the sandbox document specified in
settings.USER_ONBOARDING_SANDBOX_DOCUMENT.
2026-02-23 16:29:08 +01:00
Sylvain Boissel
5d5ac0c1c8 (backend) allow the duplication of subpages
Adds a new with_descendants parameter to the doc duplication API.
The logic of the duplicate() method has been moved to a new
internal _duplicate_document() method to allow for recursion.
Adds unit tests for the new feature.
2026-02-23 15:31:19 +01:00
Stephan Meijer
d0b756550b 👷(docker) add arm64 platform support for image builds
Signed-off-by: Stephan Meijer <me@stephanmeijer.com>
2026-02-19 17:38:18 +01:00
Cyril
010ed4618a (frontend) add floating bar with collapse button
Add sticky floating bar at top of document with leftpanelcollapse btn
2026-02-19 13:10:48 +01:00
Chaïb Martinez
c0994d7d1f (tracking) add UTM parameters to shared document links
Add utm_source=docssharelink and utm_campaign={docId} query parameters
to shared document links (copy link and invitation emails).

Signed-off-by: Chaïb Martinez <chaibax@gmail.com>
2026-02-19 10:43:48 +01:00
708 changed files with 15512 additions and 5489 deletions

3
.github/.trivyignore vendored Normal file
View File

@@ -0,0 +1,3 @@
CVE-2026-26996
CVE-2026-27903
CVE-2026-27904

View File

@@ -20,7 +20,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Create empty source files
run: |
touch src/backend/locale/django.pot
@@ -48,7 +48,7 @@ jobs:
CROWDIN_BASE_PATH: "../src/"
# frontend i18n
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}

View File

@@ -20,10 +20,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
# Backend i18n
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13.3"
cache: "pip"
@@ -33,7 +33,7 @@ jobs:
run: pip install --user .
working-directory: src/backend
- name: Restore the mail templates
uses: actions/cache@v4
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -49,7 +49,7 @@ jobs:
DJANGO_CONFIGURATION=Build python manage.py makemessages -a --keep-pot
# frontend i18n
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}

View File

@@ -20,16 +20,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
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
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node_version }}
- name: Install dependencies
@@ -37,7 +37,7 @@ jobs:
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
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
@@ -50,10 +50,10 @@ jobs:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Restore the mail templates
uses: actions/cache@v4
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -61,7 +61,7 @@ jobs:
- name: Setup Node.js
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node_version }}
@@ -79,7 +79,7 @@ jobs:
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}

View File

@@ -11,120 +11,54 @@ on:
pull_request:
branches:
- "main"
- "ci/trivy-fails"
env:
DOCKER_USER: 1001:127
SHOULD_PUSH: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
permissions:
contents: read
jobs:
build-and-push-backend:
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-backend
- name: Login to DockerHub
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- 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 }}"
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
target: backend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Cleanup Docker after build
if: always()
run: |
docker system prune -af
docker volume prune -f
uses: ./.github/workflows/docker-publish.yml
permissions:
contents: read
secrets: inherit
with:
image_name: lasuite/impress-backend
context: .
file: Dockerfile
target: backend-production
should_push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
docker_user: 1001:127
build-and-push-frontend:
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-frontend
- name: Login to DockerHub
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- 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 }}"
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}
PUBLISH_AS_MIT=false
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Cleanup Docker after build
if: always()
run: |
docker system prune -af
docker volume prune -f
uses: ./.github/workflows/docker-publish.yml
permissions:
contents: read
secrets: inherit
with:
image_name: lasuite/impress-frontend
context: .
file: src/frontend/Dockerfile
target: frontend-production
arm64_reuse_amd64_build_arg: "FRONTEND_IMAGE"
should_push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
docker_user: 1001:127
build-and-push-y-provider:
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-y-provider
- name: Login to DockerHub
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
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-y-provider:${{ github.sha }}"
- name: Build and push
uses: docker/build-push-action@v6
with:
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' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Cleanup Docker after build
if: always()
run: |
docker system prune -af
docker volume prune -f
uses: ./.github/workflows/docker-publish.yml
permissions:
contents: read
secrets: inherit
with:
image_name: lasuite/impress-y-provider
context: .
file: src/frontend/servers/y-provider/Dockerfile
target: y-provider
should_push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
docker_user: 1001:127
notify-argocd:
needs:

142
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,142 @@
name: Build and Push Container Image
description: Build and push a container image based on the input arguments provided
"on":
workflow_call:
inputs:
image_name:
type: string
required: true
description: The suffix for the image name, without the registry and without the repository path.
context:
type: string
required: true
description: The path to the context to start `docker build` into.
file:
type: string
required: true
description: The path to the Dockerfile
target:
type: string
required: false
default: ""
description: The Dockerfile target stage to build the image for.
should_push:
type: boolean
required: false
default: false
description: if the image should be pushed on the docker registry
docker_user:
type: string
required: false
default: ""
description: The docker_user ARGUMENT to pass to the build step
arm64_reuse_amd64_build_arg:
type: string
required: false
default: ""
description: "Build arg name to pass first amd64 tag to arm64 build (skips arch-independent build steps)"
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: ${{ inputs.should_push }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.image_name }}
- name: Generate platform-specific tags
id: platform-tags
run: |
AMD64_TAGS=$(echo "${{ steps.meta.outputs.tags }}" | sed 's/$/-amd64/')
ARM64_TAGS=$(echo "${{ steps.meta.outputs.tags }}" | sed 's/$/-arm64/')
FIRST_AMD64_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -1)-amd64
{
echo "amd64<<EOF"
echo "$AMD64_TAGS"
echo "EOF"
echo "arm64<<EOF"
echo "$ARM64_TAGS"
echo "EOF"
echo "amd64_first=$FIRST_AMD64_TAG"
} >> "$GITHUB_OUTPUT"
# - name: Run trivy scan
# if: ${{ vars.TRIVY_SCAN_ENABLED }} == 'true'
# uses: numerique-gouv/action-trivy-cache@main
# with:
# docker-build-args: "--target ${{ inputs.target }} -f ${{ inputs.file }}"
# docker-image-name: "docker.io/${{ inputs.image_name }}:${{ github.sha }}"
# trivyignores: ./.github/.trivyignore
- name: Build and push (amd64)
if: ${{ inputs.should_push }}||${{ vars.TRIVY_SCAN_ENABLED }} != 'true'
uses: docker/build-push-action@v6
with:
context: ${{ inputs.context }}
file: ${{ inputs.file }}
target: ${{ inputs.target }}
platforms: linux/amd64
build-args: |
DOCKER_USER=${{ inputs.docker_user }}
PUBLISH_AS_MIT=false
push: ${{ inputs.should_push }}
provenance: false
tags: ${{ steps.platform-tags.outputs.amd64 }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push (arm64)
if: ${{ inputs.should_push }}
uses: docker/build-push-action@v6
with:
context: ${{ inputs.context }}
file: ${{ inputs.file }}
target: ${{ inputs.target }}
platforms: linux/arm64
build-args: |
DOCKER_USER=${{ inputs.docker_user }}
PUBLISH_AS_MIT=false
${{ inputs.arm64_reuse_amd64_build_arg && format('{0}={1}', inputs.arm64_reuse_amd64_build_arg, steps.platform-tags.outputs.amd64_first) || '' }}
push: ${{ inputs.should_push }}
provenance: false
tags: ${{ steps.platform-tags.outputs.arm64 }}
labels: ${{ steps.meta.outputs.labels }}
- name: Create multi-arch manifests
if: ${{ inputs.should_push }}
id: create-manifest
run: |
IMAGE="${{ inputs.image_name }}"
readarray -t TAGS <<< "${{ steps.meta.outputs.tags }}"
FIRST_TAG=""
for tag in "${TAGS[@]}"; do
[ -z "$tag" ] && continue
docker buildx imagetools create -t "$tag" \
"${tag}-amd64" "${tag}-arm64"
if [ -z "$FIRST_TAG" ]; then
FIRST_TAG="$tag"
fi
done
# Get the digest of the multi-arch manifest for attestation
# Note: --format '{{.Manifest.Digest}}' is broken (docker/buildx#1175),
# so we compute it from the raw manifest JSON instead.
if [ -n "$FIRST_TAG" ]; then
DIGEST="sha256:$(docker buildx imagetools inspect "$FIRST_TAG" --raw | sha256sum | awk '{print $1}')"
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
fi
- name: Cleanup Docker after build
if: always()
run: |
docker system prune -af
docker volume prune -f

View File

@@ -22,7 +22,11 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
@@ -45,6 +49,7 @@ jobs:
with:
context: .
target: backend-production
platforms: linux/amd64,linux/arm64
build-args: DOCKER_USER=${{ env.DOCKER_USER }}
push: true
tags: ${{ steps.meta.outputs.tags }}
@@ -63,7 +68,11 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
@@ -87,6 +96,7 @@ jobs:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
platforms: linux/amd64,linux/arm64
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}
PUBLISH_AS_MIT=false
@@ -107,7 +117,11 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
@@ -131,6 +145,7 @@ jobs:
context: .
file: ./src/frontend/servers/y-provider/Dockerfile
target: y-provider
platforms: linux/amd64,linux/arm64
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: true
tags: ${{ steps.meta.outputs.tags }}

View File

@@ -15,7 +15,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
-
name: Helmfile lint
shell: bash

View File

@@ -23,15 +23,15 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
@@ -47,14 +47,14 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
@@ -69,15 +69,15 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
@@ -111,15 +111,15 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
@@ -157,11 +157,11 @@ jobs:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Detect relevant changes
id: changes
uses: dorny/paths-filter@v2
uses: dorny/paths-filter@v3
with:
filters: |
lock:
@@ -170,7 +170,7 @@ jobs:
- 'src/frontend/apps/impress/**'
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
@@ -178,7 +178,7 @@ jobs:
- name: Setup Node.js
if: steps.changes.outputs.lock == 'true' || steps.changes.outputs.app == 'true'
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22.x"
@@ -205,14 +205,14 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Restore the frontend cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}

View File

@@ -19,7 +19,7 @@ jobs:
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: show
@@ -46,7 +46,7 @@ jobs:
github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 50
- name: Check that the CHANGELOG has been modified in the current branch
@@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
@@ -70,7 +70,7 @@ jobs:
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install codespell
run: pip install --user codespell
- name: Check for typos
@@ -92,9 +92,9 @@ jobs:
working-directory: src/backend
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13.3"
cache: "pip"
@@ -143,10 +143,11 @@ jobs:
AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_S3_ACCESS_KEY_ID: impress
AWS_S3_SECRET_ACCESS_KEY: password
DB_PSYCOPG_POOL_MAX_SIZE: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Create writable /data
run: |
@@ -154,7 +155,7 @@ jobs:
sudo mkdir -p /data/static
- name: Restore the mail templates
uses: actions/cache@v4
uses: actions/cache@v5
id: mail-templates
with:
path: "src/backend/core/templates/mail"
@@ -190,7 +191,7 @@ jobs:
mc version enable impress/impress-media-storage"
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13.3"
cache: "pip"

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -6,25 +6,86 @@ and this project adheres to
## [Unreleased]
## [v4.8.0] - 2026-03-13
### Added
- ✨(backend) add a is_first_connection flag to the User model #1938
- ✨(frontend) add onboarding modal with help menu button #1868
### Changed
- ♿(frontend) localize LaGaufre label fallback in Docs #1979
- ✨(backend) add a migration cleaning on-boarding document accesses #1971
- ⬆️(frontend) upgrade Next.js to v16 #1980
- ♿️(frontend) fix aria-label and landmark on document banner state #1986
- 🌐(i18n) add "new window" translation key for waffle aria-label #1984
### Fixed
- 🐛(backend) create a link_trace record for on-boarding documents #1971
- 🐛(backend) manage race condition when creating sandbox document #1971
- 🐛(frontend) fix flickering left panel #1989
- ♿️(frontend) improve doc tree keyboard navigation #1981
- 🔧(helm) allow specific env var for the backend and celery deploy
## [v4.7.0] - 2026-03-09
### Added
- ✨(helm) allow all keys in configMap as env var #1872
### Changed
- 📝(docs) improve README and add documentation hub #1870
- ♿️(frontend) restore focus to triggers after closing menus and modals #1863
- 🚸(frontend) change position elements toolbar #1957
- ♿️(frontend) add focus on open to modals #1948
### Fixed
- 🐛(frontend) analytic feature flags problem #1953
- 🐛(frontend) fix home collapsing panel #1954
- 🐛(frontend) fix disabled color on icon Dropdown #1950
- 🐛(frontend) fix zIndex table of content #1949
- 🐛(frontend) fix bug when language not supported by BN #1957
- 🐛 (backend) prevent privileged users from requesting access #1898
## [v4.6.0] - 2026-03-03
### Added
- ✨(frontend) integrate new Blocknote AI feature #1847
- 👷(docker) add arm64 platform support for image builds #1901
- ✨(tracking) add UTM parameters to shared document links #1896
- ✨(frontend) add floating bar with leftpanel collapse button #1876
- ✨(frontend) Can print a doc #1832
- ✨(backend) manage reconciliation requests for user accounts #1878
- 👷(CI) add GHCR workflow for forked repo testing #1851
- ✨(frontend) Move doc modal #1886
- ⚡️(backend) remove content from Document serializer when asked #1910
- ✨(backend) allow the duplication of subpages #1893
- ✨(backend) Onboarding docs for new users #1891
- 🩺(trivy) add trivyignore file and add minimatch CVE #1915
- 🚩 Add feature flags for the AI feature #1922
- 🍱(frontend) add icons ui-kit #1943
### Changed
- ♿️(frontend) prevent dates from being focusable #1855
- ♿️(frontend) Focus main container after navigation #1854
- ♿️(frontend) Focus main container after navigation #1864
- 💄(frontend) align colors and logo with ui-kit v2 #1869
- 🚸(backend) sort user search results by proximity with the active user #1802
- 🚸(oidc) ignore case when fallback on email #1880
- ⚡️(CI) optimize Docker Hub workflow #1919
### Fixed
- 🐛(frontend) fix broadcast store sync #1846
- 🐛(helm) use celery resources instead of backend resources
- 🐛(helm) reverse liveness and readiness for backend deployment
- 🐛(helm) use celery resources instead of backend resources #1887
- 🐛(helm) reverse liveness and readiness for backend deployment #1887
- 🐛(y-provider) use CONVERSION_FILE_MAX_SIZE settings #1913
- 🐛(frontend) fix callout block spacing for old browsers #1914
## [v4.5.0] - 2026-01-28
@@ -265,7 +326,7 @@ and this project adheres to
- ♻️(frontend) Refactor Auth component for improved redirection logic #1461
- ♻️(frontend) replace Arial font-family with token font #1411
- ♿(frontend) improve accessibility:
- ♿(frontend) enable enter key to open documentss #1354
- ♿(frontend) enable enter key to open documents #1354
- ♿(frontend) improve modal a11y: structure, labels, title #1349
- ♿improve NVDA navigation in DocShareModal #1396
- ♿ improve accessibility by adding landmark roles to layout #1394
@@ -473,10 +534,10 @@ and this project adheres to
- ✨(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
- ✨(backend) allow theme customization 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
- ✨(backend) integrate malware_detection from django-lasuite #936
- 🏗️(frontend) Footer configurable #959
- 🩺(CI) add lint spell mistakes #954
- ✨(frontend) create generic theme #792
@@ -1044,7 +1105,10 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.5.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.8.0...main
[v4.8.0]: https://github.com/suitenumerique/docs/releases/v4.8.0
[v4.7.0]: https://github.com/suitenumerique/docs/releases/v4.7.0
[v4.6.0]: https://github.com/suitenumerique/docs/releases/v4.6.0
[v4.5.0]: https://github.com/suitenumerique/docs/releases/v4.5.0
[v4.4.0]: https://github.com/suitenumerique/docs/releases/v4.4.0
[v4.3.0]: https://github.com/suitenumerique/docs/releases/v4.3.0

View File

@@ -95,8 +95,8 @@ 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.
If you find an 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).
The project is licensed 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 licensed with GNU AFFERO GENERAL PUBLIC LICENSE Version 3 and proprietary license if you are a [sponsor](https://www.blocknotejs.org/pricing).

View File

@@ -14,13 +14,6 @@ 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
@@ -151,7 +144,7 @@ RUN rm -rf /var/cache/apk/*
ARG IMPRESS_STATIC_ROOT=/data/static
# Gunicorn
# Gunicorn - not used by default but configuration file is provided
RUN mkdir -p /usr/local/etc/gunicorn
COPY docker/files/usr/local/etc/gunicorn/impress.py /usr/local/etc/gunicorn/impress.py
@@ -165,5 +158,18 @@ COPY --from=link-collector ${IMPRESS_STATIC_ROOT} ${IMPRESS_STATIC_ROOT}
# Copy impress mails
COPY --from=mail-builder /mail/backend/core/templates/mail /app/core/templates/mail
# The default command runs gunicorn WSGI server in impress's main module
CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/impress.py", "impress.wsgi:application"]
# The default command runs uvicorn ASGI server in dics's main module
# WEB_CONCURRENCY: number of workers to run <=> --workers=4
ENV WEB_CONCURRENCY=4
CMD [\
"uvicorn",\
"--app-dir=/app",\
"--host=0.0.0.0",\
"--timeout-graceful-shutdown=300",\
"--limit-max-requests=20000",\
"--lifespan=off",\
"impress.asgi:application"\
]
# To run using gunicorn WSGI server use this instead:
#CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/conversations.py", "impress.wsgi:application"]

284
README.md
View File

@@ -3,226 +3,238 @@
<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/CONTRIBUTING.md">
<img alt="PRs Welcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg"/>
</a>
<a href="https://github.com/suitenumerique/docs/blob/main/LICENSE">
<img alt="MIT License" 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>
# La Suite Docs : Collaborative Text Editing
Docs, where your notes can become knowledge through live collaboration.
<p align="center">
<a href="https://matrix.to/#/#docs-official:matrix.org">Chat on Matrix</a> •
<a href="/docs/">Documentation</a> •
<a href="#try-docs">Try Docs</a> •
<a href="mailto:docs@numerique.gouv.fr">Contact us</a>
</p>
<img src="/docs/assets/docs_live_collaboration_light.gif" width="100%" align="center"/>
# La Suite Docs: Collaborative Text Editing
## Why use Docs ❓
Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing.
**Docs, where your notes can become knowledge through live collaboration.**
### 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!
Docs is an open-source collaborative editor that helps teams write, organize, and share knowledge together - in real time.
### 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.
![Live collaboration demo](/docs/assets/docs_live_collaboration_light.gif)
### Self-host
#### 🚀 Docs is easy to install on your own servers
We use Kubernetes for our [production instance](https://docs.numerique.gouv.fr/) but also support Docker Compose. The community contributed a couple other methods (Nix, YunoHost etc.) check out the [docs](/docs/installation/README.md) to get detailed instructions and examples.
## What is Docs?
#### 🌍 Known instances
We hope to see many more, here is an incomplete list of public Docs instances. Feel free to make a PR to add ones that are not listed below🙏
Docs is an open-source alternative to tools like Notion or Google Docs, focused on:
| Url | Org | Public |
| --- | --- | ------- |
| [docs.numerique.gouv.fr](https://docs.numerique.gouv.fr/) | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up|
| [docs.suite.anct.gouv.fr](https://docs.suite.anct.gouv.fr/) | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up|
| [notes.demo.opendesk.eu](https://notes.demo.opendesk.eu) | ZenDiS | Demo instance of OpenDesk. Request access to get credentials |
| [notes.liiib.re](https://notes.liiib.re/) | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
| [docs.federated.nexus](https://docs.federated.nexus/) | federated.nexus | Public instance, but you have to [sign up for a Federated Nexus account](https://federated.nexus/register/). |
| [docs.demo.mosacloud.eu](https://docs.demo.mosacloud.eu/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
- Real-time collaboration
- Clean, structured documents
- Knowledge organization
- Data ownership & self-hosting
#### ⚠️ Advanced features
For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under GPL 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.
***Built for public organizations, companies, and open communities.***
## Getting started 🔧
## Why use Docs?
### Test it
### Writing
You can test Docs on your browser by visiting this [demo document](https://docs.la-suite.eu/docs/9137bbb5-3e8a-4ff7-8a36-fcc4e8bd57f4/)
- Rich-text & Markdown editing
- Slash commands & block system
- Beautiful formatting
- Offline editing
- Optional AI writing helpers (rewrite, summarize, translate, fix typos)
### Run Docs locally
### Collaboration
> ⚠️ 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.
- Live cursors & presence
- Comments & sharing
- Granular access control
**Prerequisite**
### Knowledge management
Make sure you have a recent version of Docker and [Docker Compose](https://docs.docker.com/compose/install) installed on your laptop, then type:
- Subpages & hierarchy
- Searchable content
```shellscript
$ docker -v
### Export/Import & interoperability
Docker version 20.10.2, build 2291f61
- Import to `.docx` and `.md`
- Export to `.docx`, `.odt`, `.pdf`
$ docker compose version
## Try Docs
Docker Compose version v2.32.4
Experience Docs instantly - no installation required.
- 🔗 [Open a live demo document][demo]
- 🌍 [Browse public instances][instances]
[demo]: https://docs.la-suite.eu/docs/9137bbb5-3e8a-4ff7-8a36-fcc4e8bd57f4/
[instances]: /docs/instances.md
## Self-hosting
Docs supports Kubernetes, Docker Compose, and community-provided methods such as Nix and YunoHost.
Get started with self-hosting: [Installation guide](/docs/installation/README.md)
> [!WARNING]
> Some advanced features (for example: `Export as PDF`) rely on XL packages from Blocknote.
> These packages are licensed under GPL and are **not MIT-compatible**
>
> You can run Docs **without these packages** by building with:
>
> ```bash
> PUBLISH_AS_MIT=true
> ```
>
> This builds an image of Docs without non-MIT features.
>
> More details can be found in [environment variables](/docs/env.md)
## Local Development (for contributors)
Run Docs locally for development and testing.
> [!WARNING]
> This setup is intended **for development and testing only**.
> It uses Minio as an S3-compatible storage backend, but any S3-compatible service can be used.
### Prerequisites
- Docker
- Docker Compose
- GNU Make
Verify installation:
```bash
docker -v
docker compose version
```
> ⚠️ You may need to run the following commands with `sudo`, but this can be avoided by adding your user to the local `docker` group.
> If you encounter permission errors, you may need to use `sudo`, or add your user to the `docker` group.
**Project bootstrap**
### Bootstrap the project
The easiest way to start working on the project is to use [GNU Make](https://www.gnu.org/software/make/):
The easiest way to start is using GNU Make:
```shellscript
$ make bootstrap FLUSH_ARGS='--no-input'
```bash
make bootstrap FLUSH_ARGS='--no-input'
```
This command builds the `app-dev` and `frontend-dev` containers, installs dependencies, performs database migrations and compiles translations. It's a good idea to use this command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues.
This builds the `app-dev` and `frontend-dev` containers, installs dependencies, runs database migrations, and compiles translations.
Your Docker services should now be up and running 🎉
It is recommended to run this command after pulling new code.
You can access the project by going to <http://localhost:3000>.
You will be prompted to log in. The default credentials are:
Start services:
```bash
make run
```
Open <https://localhost:3000>
Default credentials (development only):
```md
username: impress
password: impress
```
📝 Note that if you need to run them afterwards, you can use the eponymous Make rule:
### Frontend development mode
```shellscript
$ make run
For frontend work, running outside Docker is often more convenient:
```bash
make frontend-development-install
make run-frontend-development
```
⚠️ For the frontend developer, it is often better to run the frontend in development mode locally.
### Backend only
To do so, install the frontend dependencies with the following command:
Starting all services except the frontend container:
```shellscript
$ make frontend-development-install
```bash
make run-backend
```
And run the frontend locally in development mode with the following command:
### Tests & Linting
```shellscript
$ make run-frontend-development
```bash
make frontend-test
make frontend-lint
```
To start all the services, except the frontend container, you can use the following command:
### Demo content
```shellscript
$ make run-backend
Create a basic demo site:
```bash
make demo
```
To execute frontend tests & linting only
```shellscript
$ make frontend-test
$ make frontend-lint
### More Make targets
To check all available Make rules:
```bash
make help
```
**Adding content**
### Django admin
You can create a basic demo site by running this command:
Create a superuser:
```shellscript
$ make demo
```bash
make superuser
```
Finally, you can check all available Make rules using this command:
Admin UI: <http://localhost:8071/admin>
```shellscript
$ make help
```
## Contributing
**Django admin**
This project is community-driven and PRs are welcome.
You can access the Django admin site at:
- [Contribution guide](CONTRIBUTING.md)
- [Translations](https://crowdin.com/project/lasuite-docs)
- [Chat with us!](https://matrix.to/#/#docs-official:matrix.org)
<http://localhost:8071/admin>.
## Roadmap
You first need to create a superuser account:
Curious where Docs is headed?
```shellscript
$ make superuser
```
## Feedback 🙋‍♂️🙋‍♀️
We'd love to hear your thoughts, and hear about your experiments, so come and say hi on [Matrix](https://matrix.to/#/#docs-official:matrix.org).
## Roadmap 💡
Want to know where the project is headed? [🗺️ Checkout our roadmap](https://github.com/orgs/numerique-gouv/projects/13/views/11)
Explore upcoming features, priorities and long-term direction on our [public roadmap](https://docs.numerique.gouv.fr/docs/d1d3788e-c619-41ff-abe8-2d079da2f084/).
## License 📝
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 license 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.
```
While Docs is a public-driven initiative, our license choice is an invitation for private sector actors to use, sell and contribute to 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!
Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [ProseMirror](https://prosemirror.net/), [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/).
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.
Docs is the result of a joint initiative led by the French 🇫🇷 ([DINUM](https://www.numerique.gouv.fr/dinum/)) Government and German 🇩🇪 government ([ZenDiS](https://zendis.de/)).
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱), feel free to [contact us](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%"/>
<img src="/docs/assets/europe_opensource.png" width="50%"/ alt="Europe Opensource">
</p>

View File

@@ -16,6 +16,12 @@ the following command inside your docker container:
## [Unreleased]
## [4.6.0] - 2026-02-27
- ⚠️ Some setup have changed to offer a bigger flexibility and consistency, overriding the favicon and logo are now from the theme configuration.
https://github.com/suitenumerique/docs/blob/f24b047a7cc146411412bf759b5b5248a45c3d99/src/backend/impress/configuration/theme/default.json#L129-L161
## [4.0.0] - 2025-11-26
- ⚠️ We updated `@gouvfr-lasuite/ui-kit` to `0.18.0`, so if you are customizing Docs with a css layer or with a custom template, you need to update your customization to follow the new design system structure.
@@ -62,5 +68,5 @@ service.
- 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
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

@@ -22,44 +22,34 @@ services:
ports:
- "1081:1080"
rustfs:
minio:
user: ${DOCKER_USER:-1000}
image: rustfs/rustfs:latest
image: minio/minio
environment:
- RUSTFS_ACCESS_KEY=rustfsadmin
- RUSTFS_SECRET_KEY=rustfsadmin
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CORS_ALLOWED_ORIGINS=*
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- MINIO_ROOT_USER=impress
- MINIO_ROOT_PASSWORD=password
ports:
- "9000:9000"
- "9001:9001"
- '9000:9000'
- '9001:9001'
healthcheck:
test:
[
"CMD",
"sh",
"-c",
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 20s
retries: 300
entrypoint: ""
command: minio server --console-address :9001 /data
volumes:
- ./data/media:/data
createbuckets:
image: minio/mc
depends_on:
rustfs:
minio:
condition: service_healthy
restart: true
entrypoint: >
sh -c "
/usr/bin/mc alias set impress http://rustfs:9000 rustfsadmin rustfsadmin && \
/usr/bin/mc alias set impress http://minio:9000 impress password && \
/usr/bin/mc mb impress/impress-media-storage && \
/usr/bin/mc version enable impress/impress-media-storage && \
exit 0;"
@@ -91,16 +81,16 @@ services:
- ./src/backend:/app
- ./data/static:/data/static
depends_on:
postgresql:
condition: service_healthy
restart: true
mailcatcher:
condition: service_started
redis:
condition: service_started
createbuckets:
condition: service_started
postgresql:
condition: service_healthy
restart: true
mailcatcher:
condition: service_started
redis:
condition: service_started
createbuckets:
condition: service_started
celery-dev:
user: ${DOCKER_USER:-1000}
image: impress:backend-development
@@ -141,7 +131,7 @@ services:
frontend-development:
user: "${DOCKER_USER:-1000}"
build:
build:
context: .
dockerfile: ./src/frontend/Dockerfile
target: impress-dev
@@ -171,13 +161,13 @@ services:
image: node:22
user: "${DOCKER_USER:-1000}"
environment:
HOME: /tmp
HOME: /tmp
volumes:
- ".:/app"
y-provider-development:
user: ${DOCKER_USER:-1000}
build:
build:
context: .
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
target: y-provider-development
@@ -219,11 +209,7 @@ services:
- --health-enabled=true
- --metrics-enabled=true
healthcheck:
test:
[
"CMD-SHELL",
'exec 3<>/dev/tcp/localhost/9000; echo -e "GET /health/live HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" >&3; grep "HTTP/1.1 200 OK" <&3',
]
test: ['CMD-SHELL', 'exec 3<>/dev/tcp/localhost/9000; echo -e "GET /health/live HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" >&3; grep "HTTP/1.1 200 OK" <&3']
start_period: 5s
interval: 1s
timeout: 2s
@@ -237,7 +223,7 @@ services:
KC_DB_PASSWORD: pass
KC_DB_USERNAME: impress
KC_DB_SCHEMA: public
PROXY_ADDRESS_FORWARDING: "true"
PROXY_ADDRESS_FORWARDING: 'true'
ports:
- "8080:8080"
depends_on:

View File

@@ -17,9 +17,9 @@ server {
proxy_set_header X-Amz-Date $authDate;
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
# Get resource from rustfs
proxy_pass http://rustfs:9000/impress-media-storage/;
proxy_set_header Host rustfs:9000;
# 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;
}
@@ -30,7 +30,7 @@ server {
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 "";

39
docs/README.md Normal file
View File

@@ -0,0 +1,39 @@
# Docs Documentation
Welcome to the official documentation for Docs.
This documentation is organized by topic and audience.
Use the section below to quickly find what you are looking for.
---
## Table of Contents
- Getting started
- [System requirements](system-requirements.md)
- [Installation overview](installation/README.md)
- [Docker Compose deployment](installation/compose.md)
- [Docker Compose examples](examples/compose/)
- [Kubernetes deployment](installation/kubernetes.md)
- [Helm values examples](examples/helm/)
- Configuration
- [Environment variables](env.md)
- [Customization](customization.md)
- [Language configuration](languages-configuration.md)
- [Search configuration](search.md)
- Architecture & design
- [Architecture overview](architecture.md)
- [Architectural Decision Records (ADR)](adr/)
- Usage & operations
- [Public instances](instances.md)
- [Releases & upgrades](release.md)
- [Troubleshooting](troubleshoot.md)
- Project & product
- [Roadmap](roadmap.md)
- Assets
- [Branding & visuals](assets/)

View File

@@ -7,17 +7,21 @@ Here we describe all environment variables that can be set for the docs applicat
These are the environment variables you can set for the `impress-backend` container.
| Option | Description | default |
|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" }
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_FEATURE_BLOCKNOTE_ENABLED | Enable Blocknote AI options | false |
| AI_FEATURE_LEGACY_ENABLED | Enable legacyAI options | true |
| AI_MODEL | AI Model to use | |
| AI_VERCEL_SDK_VERSION | The vercel AI SDK version used | 6 |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
| API_USERS_SEARCH_QUERY_MIN_LENGTH | Minimum characters to insert to search a user | 3 |
| API_USERS_SEARCH_QUERY_MIN_LENGTH | Minimum characters to insert to search a user | 3 |
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
@@ -35,13 +39,16 @@ These are the environment variables you can set for the `impress-backend` contai
| CONVERSION_API_SECURE | Require secure conversion api | false |
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
| CONVERSION_FILE_MAX_SIZE | The file max size allowed when uploaded to convert it | 20971520 (20MB) |
| CONVERSION_FILE_EXTENSIONS_ALLOWED | Extension list managed by the conversion service | [".docx", ".md"]
| CONVERSION_FILE_EXTENSIONS_ALLOWED | Extension list managed by the conversion service | [".docx", ".md"] |
| CRISP_WEBSITE_ID | Crisp website id for support | |
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
| DB_HOST | Host of the database | localhost |
| DB_NAME | Name of the database | impress |
| DB_PASSWORD | Password to authenticate with | pass |
| DB_PORT | Port of the database | 5432 |
| DB_PSYCOPG_POOL_MIN_SIZE | The psycopg min pool size | 4 |
| DB_PSYCOPG_POOL_MAX_SIZE | The psycopg max pool size | None |
| DB_PSYCOPG_POOL_TIMEOUT | The default maximum time in seconds that a client can wait to receive a connection from the pool | 3 |
| DB_USER | User to authenticate with | dinum |
| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} |
@@ -58,22 +65,22 @@ These are the environment variables you can set for the `impress-backend` contai
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
| DJANGO_EMAIL_PORT | Port used to connect to email host | |
| DJANGO_EMAIL_URL_APP | Url used in the email to go to the app | |
| DJANGO_EMAIL_URL_APP | Url used in the email to go to the app | |
| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false |
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
| DJANGO_SECRET_KEY | Secret key | |
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
| DOCSPEC_API_URL | URL to endpoint of DocSpec conversion API | |
| DOCSPEC_API_URL | URL to endpoint of DocSpec conversion API | |
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
| FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_JS_URL | To add a external js file to the app | |
| FRONTEND_JS_URL | To add a external js file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
| FRONTEND_THEME | Frontend theme to use | |
| LANGUAGE_CODE | Default language | en-us |
| LANGFUSE_SECRET_KEY | The Langfuse secret key used by the sdk | None |
| LANGFUSE_PUBLIC_KEY | The Langfuse public key used by the sdk | None |
| LANGFUSE_BASE_URL | The Langfuse base url used by the sdk | None |
| LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend |
| LASUITE_MARKETING_BACKEND | Backend used when SIGNUP_NEW_USER_TO_MARKETING_EMAIL is True. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | lasuite.marketing.backends.dummy.DummyBackend |
| LASUITE_MARKETING_PARAMETERS | The parameters to configure LASUITE_MARKETING_BACKEND. See https://github.com/suitenumerique/django-lasuite/blob/main/documentation/how-to-use-marketing-backend.md | {} |
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
@@ -120,11 +127,12 @@ These are the environment variables you can set for the `impress-backend` contai
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
| USER_ONBOARDING_DOCUMENTS | A list of documents IDs for which a read-only access will be created for new s | [] |
| USER_ONBOARDING_SANDBOX_DOCUMENT | ID of a template sandbox document that will be duplicated for new users | |
| USER_RECONCILIATION_FORM_URL | URL of a third-party form for user reconciliation requests | |
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
| Y_PROVIDER_API_KEY | Y provider API key | |
## impress-frontend image
These are the environment variables you can set to build the `impress-frontend` image.
@@ -135,31 +143,31 @@ If you want to build the Docker image, this variable is used as an argument in t
Example:
```
```bash
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:
```
```bash
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 |
| 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`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
* `xl-multi-column`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE).
* `xl-docx-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
* `xl-multi-column`: [GPL](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/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 or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.

View File

@@ -13,7 +13,7 @@ Please follow the instructions [here](/docs/installation/compose.md).
⚠️ Please keep in mind that we do not use it ourselves in production. Let us know in the issues if you run into troubles, we'll try to help.
## Other ways to install Docs
Community members have contributed several other ways to install Docs. While we owe them a big thanks 🙏, please keep in mind we (Docs maintainers) can't provide support on these installation methods as we don't use them ourselves and there are two many options out there for us to keep track of. Of course you can contact the contributors and the broader community for assistance.
Community members have contributed several other ways to install Docs. While we owe them a big thanks 🙏, please keep in mind we (Docs maintainers) can't provide support on these installation methods as we don't use them ourselves and there are too many options out there for us to keep track of. Of course you can contact the contributors and the broader community for assistance.
Here is the list of other methods in alphabetical order:
- Coop-Cloud: [code](https://git.coopcloud.tech/coop-cloud/lasuite-docs)

View File

@@ -134,10 +134,12 @@ DJANGO_EMAIL_URL_APP=<url used in email templates to go to the app> # e.g. "http
Built-in AI actions let users generate, summarize, translate, and correct content.
AI is disabled by default. To enable it, the following environment variables must be set in in `env.d/backend`:
AI is disabled by default. To enable it, the following environment variables must be set in `env.d/backend`:
```env
AI_FEATURE_ENABLED=true # is false by default
AI_FEATURE_BLOCKNOTE_ENABLED=true # is false by default
AI_FEATURE_LEGACY_ENABLED=true # is true by default, AI_FEATURE_ENABLED must be set to true to enable it
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=<API key>
AI_MODEL=<model used> e.g. llama
@@ -150,7 +152,7 @@ You can [customize your Docs instance](../theming.md) with your own theme and cu
The following environment variables must be set in `env.d/backend`:
```env
FRONTEND_THEME=default # name of your theme built with cuningham
FRONTEND_THEME=default # name of your theme built with Cunningham
FRONTEND_CSS_URL=https://storage.yourdomain.tld/themes/custom.css # custom css
```
@@ -204,7 +206,7 @@ Replace `<admin email>` with the email of your admin user and generate a secure
Your docs instance is now available on the domain you defined, https://docs.yourdomain.tld.
THe admin interface is available on https://docs.yourdomain.tld/admin with the admin user you just created.
The admin interface is available on https://docs.yourdomain.tld/admin with the admin user you just created.
## How to upgrade your Docs application

View File

@@ -250,4 +250,4 @@ minio-dev-backend-minio-api <none> docs-minio.127.0.0.1.nip.io
minio-dev-backend-minio-console <none> docs-minio-console.127.0.0.1.nip.io localhost 80, 443 8m48s
```
You can use Docs at https://docs.127.0.0.1.nip.io. The provisionning user in keycloak is docs/docs.
You can use Docs at https://docs.127.0.0.1.nip.io. The provisioning user in keycloak is docs/docs.

77
docs/instances.md Normal file
View File

@@ -0,0 +1,77 @@
# 🌍 Public Docs Instances
This page lists known public instances of **Docs**.
These instances are operated by different organizations and may have different access policies.
If you run a public instance and would like it listed here, feel free to open a pull request.
---
## 🏛️ Public Organizations
### docs.numerique.gouv.fr
**Organization:** DINUM
**Audience:** French public agents working for central administration and extended public sphere
**Access:** ProConnect account required
<https://docs.numerique.gouv.fr/>
### docs.suite.anct.gouv.fr
**Organization:** ANCT
**Audience:** French public agents working for territorial administration and extended public sphere
**Access:** ProConnect account required
<https://docs.suite.anct.gouv.fr/>
### notes.demo.opendesk.eu
**Organization:** ZenDiS
**Type:** OpenDesk demo instance
**Access:** Request credentials
<https://notes.demo.opendesk.eu/>
---
## 🏢 Private Sector
### docs.demo.mosacloud.eu
**Organization:** mosa.cloud
**Type:** Demo instance
<https://docs.demo.mosacloud.eu/>
### notes.liiib.re
**Organization:** lasuite.coop
**Access:** Public demo
**Notes:** Content and accounts reset monthly
<https://notes.liiib.re/>
### notes.lasuite.coop
**Organization:** lasuite.coop
**Access:** Public
<https://notes.lasuite.coop/>
---
## 🤝 NGOs
### docs.federated.nexus
**Organization:** federated.nexus
**Access:** Public with account registration
<https://docs.federated.nexus/>
---
## Add your instance
To add your instance:
1. Fork the repository
2. Edit `docs/instances.md`
3. Add your instance following the existing format
4. Open a pull request
Thank you for helping grow the Docs ecosystem ❤️

View File

@@ -27,9 +27,9 @@ IMPRESS_BASE_URL="http://localhost:8072"
# Media
STORAGES_STATICFILES_BACKEND=django.contrib.staticfiles.storage.StaticFilesStorage
AWS_S3_ENDPOINT_URL=http://rustfs:9000
AWS_S3_ACCESS_KEY_ID=rustfsadmin
AWS_S3_SECRET_ACCESS_KEY=rustfsadmin
AWS_S3_ENDPOINT_URL=http://minio:9000
AWS_S3_ACCESS_KEY_ID=impress
AWS_S3_SECRET_ACCESS_KEY=password
MEDIA_BASE_URL=http://localhost:8083
# OIDC
@@ -64,6 +64,8 @@ USER_RECONCILIATION_FORM_URL=http://localhost:3000
# AI
AI_FEATURE_ENABLED=true
AI_FEATURE_BLOCKNOTE_ENABLED=true
AI_FEATURE_LEGACY_ENABLED=true
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama

View File

@@ -8,4 +8,5 @@ DB_HOST=postgresql
DB_NAME=impress
DB_USER=dinum
DB_PASSWORD=pass
DB_PORT=5432
DB_PORT=5432
DB_PSYCOPG_POOL_MAX_SIZE=15

View File

@@ -58,6 +58,8 @@ OIDC_REDIRECT_ALLOWED_HOSTS=["https://${DOCS_HOST}"]
# AI
#AI_FEATURE_ENABLED=true # is false by default
#AI_FEATURE_BLOCKNOTE_ENABLED=true # is false by default
#AI_FEATURE_LEGACY_ENABLED=true # is true by default, AI_FEATURE_ENABLED must be set to true to enable it
#AI_BASE_URL=https://openaiendpoint.com
#AI_API_KEY=<API key>
#AI_MODEL=<model used> e.g. llama

View File

@@ -37,15 +37,24 @@
"matchPackageNames": ["celery"],
"allowedVersions": "<5.6.0"
},
{
"groupName": "allowed pydantic-ai-slim versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["pydantic-ai-slim"],
"allowedVersions": "<1.59.0"
},
{
"groupName": "allowed langfuse versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["langfuse"],
"allowedVersions": "<3.12.0"
},
{
"enabled": false,
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@next/eslint-plugin-next",
"eslint-config-next",
"fetch-mock",
"next",
"node",
"node-fetch",
"react-resizable-panels",

View File

@@ -32,8 +32,21 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name", "language"]
read_only_fields = ["id", "email", "full_name", "short_name"]
fields = [
"id",
"email",
"full_name",
"short_name",
"language",
"is_first_connection",
]
read_only_fields = [
"id",
"email",
"full_name",
"short_name",
"is_first_connection",
]
def get_full_name(self, instance):
"""Return the full name of the user."""
@@ -225,8 +238,16 @@ class DocumentSerializer(ListDocumentSerializer):
fields = super().get_fields()
request = self.context.get("request")
if request and request.method == "POST":
fields["id"].read_only = False
if request:
if request.method == "POST":
fields["id"].read_only = False
if (
serializers.BooleanField().to_internal_value(
request.query_params.get("without_content", False)
)
is True
):
del fields["content"]
return fields
@@ -591,10 +612,13 @@ class LinkDocumentSerializer(serializers.ModelSerializer):
class DocumentDuplicationSerializer(serializers.Serializer):
"""
Serializer for duplicating a document.
Allows specifying whether to keep access permissions.
Allows specifying whether to keep access permissions,
and whether to duplicate descendant documents as well
(deep copy) or not (shallow copy).
"""
with_accesses = serializers.BooleanField(default=False)
with_descendants = serializers.BooleanField(default=False)
def create(self, validated_data):
"""

View File

@@ -27,6 +27,7 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.http import content_disposition_header
from django.utils.text import capfirst, slugify
from django.utils.translation import gettext_lazy as _
@@ -38,6 +39,7 @@ from csp.decorators import csp_update
from lasuite.malware_detection import malware_detection
from lasuite.oidc_login.decorators import refresh_oidc_access_token
from lasuite.tools.email import get_domain_from_email
from pydantic import ValidationError as PydanticValidationError
from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
from rest_framework.permissions import AllowAny
@@ -316,6 +318,25 @@ class UserViewSet(
self.serializer_class(request.user, context=context).data
)
@drf.decorators.action(
detail=False,
methods=["post"],
url_path="onboarding-done",
permission_classes=[permissions.IsAuthenticated],
)
def onboarding_done(self, request):
"""
Allows the frontend to mark the first connection as done for the current user,
e.g. after showing an onboarding message.
"""
if request.user.is_first_connection:
request.user.is_first_connection = False
request.user.save(update_fields=["is_first_connection", "updated_at"])
return drf.response.Response(
{"detail": "Onboarding marked as done."}, status=status.HTTP_200_OK
)
class ReconciliationConfirmView(APIView):
"""API endpoint to confirm user reconciliation emails.
@@ -475,6 +496,9 @@ class DocumentViewSet(
Returns: JSON response with the translated text.
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.
12. **AI Proxy**: Proxy an AI request to an external AI service.
Example: POST /api/v1.0/documents/<resource_id>/ai-proxy
### Ordering: created_at, updated_at, is_favorite, title
Example:
@@ -1208,11 +1232,7 @@ class DocumentViewSet(
@transaction.atomic
def duplicate(self, request, *args, **kwargs):
"""
Duplicate a document and store the links to attached files in the duplicated
document to allow cross-access.
Optionally duplicates accesses if `with_accesses` is set to true
in the payload.
Duplicate a document, alongside its descendants if requested.
"""
# Get document while checking permissions
document_to_duplicate = self.get_object()
@@ -1221,8 +1241,43 @@ class DocumentViewSet(
data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
user = request.user
duplicated_document = self._duplicate_document(
document_to_duplicate=document_to_duplicate,
serializer=serializer,
user=user,
)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
)
def _duplicate_document(
self,
document_to_duplicate,
serializer,
user,
new_parent=None,
):
"""
Duplicate a document and store the links to attached files in the duplicated
document to allow cross-access.
Optionally duplicates accesses if `with_accesses` is set to true
in the payload.
Optionally duplicates sub-documents if `with_descendants` is set to true in
the payload. In this case, the whole subtree of the document will be duplicated,
and the links to attached files will be stored in all duplicated documents.
The `with_accesses` option will also be applied to all duplicated documents
if `with_descendants` is set to true.
"""
with_accesses = serializer.validated_data.get("with_accesses", False)
user_role = document_to_duplicate.get_role(request.user)
with_descendants = serializer.validated_data.get("with_descendants", False)
user_role = document_to_duplicate.get_role(user)
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
base64_yjs_content = document_to_duplicate.content
@@ -1241,11 +1296,41 @@ class DocumentViewSet(
extracted_attachments & set(document_to_duplicate.attachments)
)
title = capfirst(_("copy of {title}").format(title=document_to_duplicate.title))
if not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
# If parent_duplicate is provided we must add the duplicated document as a child
if new_parent is not None:
duplicated_document = new_parent.add_child(
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document_to_duplicate,
creator=user,
**link_kwargs,
)
# Handle access duplication for this child
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document_to_duplicate
).exclude(user=user)
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
user_id=access.user_id,
team=access.team,
role=access.role,
)
for access in original_accesses
]
if accesses_to_create:
models.DocumentAccess.objects.bulk_create(accesses_to_create)
elif not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
user_role
) < choices.RoleChoices.get_priority(models.RoleChoices.EDITOR):
duplicated_document = models.Document.add_root(
creator=self.request.user,
creator=user,
title=title,
content=base64_yjs_content,
attachments=attachments,
@@ -1254,55 +1339,63 @@ class DocumentViewSet(
)
models.DocumentAccess.objects.create(
document=duplicated_document,
user=self.request.user,
user=user,
role=models.RoleChoices.OWNER,
)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
else:
duplicated_document = document_to_duplicate.add_sibling(
"right",
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document_to_duplicate,
creator=user,
**link_kwargs,
)
duplicated_document = document_to_duplicate.add_sibling(
"right",
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document_to_duplicate,
creator=request.user,
**link_kwargs,
)
# Always add the logged-in user as OWNER for root documents
if document_to_duplicate.is_root():
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
user=request.user,
role=models.RoleChoices.OWNER,
)
]
# If accesses should be duplicated, add other users' accesses as per original document
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document_to_duplicate
).exclude(user=request.user)
accesses_to_create.extend(
# Always add the logged-in user as OWNER for root documents
if document_to_duplicate.is_root():
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
user_id=access.user_id,
team=access.team,
role=access.role,
user=user,
role=models.RoleChoices.OWNER,
)
for access in original_accesses
]
# If accesses should be duplicated,
# add other users' accesses as per original document
if with_accesses and is_owner_or_admin:
original_accesses = models.DocumentAccess.objects.filter(
document=document_to_duplicate
).exclude(user=user)
accesses_to_create.extend(
models.DocumentAccess(
document=duplicated_document,
user_id=access.user_id,
team=access.team,
role=access.role,
)
for access in original_accesses
)
# Bulk create all the duplicated accesses
models.DocumentAccess.objects.bulk_create(accesses_to_create)
if with_descendants:
for child in document_to_duplicate.get_children().filter(
ancestors_deleted_at__isnull=True
):
# When duplicating descendants, attach duplicates under the duplicated_document
self._duplicate_document(
document_to_duplicate=child,
serializer=serializer,
user=user,
new_parent=duplicated_document,
)
# Bulk create all the duplicated accesses
models.DocumentAccess.objects.bulk_create(accesses_to_create)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
)
return duplicated_document
def _search_simple(self, request, text):
"""
@@ -1588,11 +1681,19 @@ class DocumentViewSet(
or serializer.validated_data["is_unsafe"]
):
extra_args.update(
{"ContentDisposition": f'attachment; filename="{file_name:s}"'}
{
"ContentDisposition": content_disposition_header(
as_attachment=True, filename=file_name
)
}
)
else:
extra_args.update(
{"ContentDisposition": f'inline; filename="{file_name:s}"'}
{
"ContentDisposition": content_disposition_header(
as_attachment=False, filename=file_name
)
}
)
file = serializer.validated_data["file"]
@@ -1763,6 +1864,45 @@ class DocumentViewSet(
return drf.response.Response(body, status=drf.status.HTTP_200_OK)
@drf.decorators.action(
detail=True,
methods=["post"],
name="Proxy AI requests to the AI provider",
url_path="ai-proxy",
throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
)
def ai_proxy(self, request, *args, **kwargs):
"""
POST /api/v1.0/documents/<resource_id>/ai-proxy
Proxy AI requests to the configured AI provider.
This endpoint forwards requests to the AI provider and returns the complete response.
"""
# Check permissions first
self.get_object()
if not settings.AI_FEATURE_ENABLED or not settings.AI_FEATURE_BLOCKNOTE_ENABLED:
raise ValidationError("AI feature is not enabled.")
ai_service = AIService()
try:
stream = ai_service.stream(request)
except PydanticValidationError as err:
logger.info("pydantic validation error: %s", err)
return drf.response.Response(
{"detail": "Invalid submitted payload"},
status=drf.status.HTTP_400_BAD_REQUEST,
)
return StreamingHttpResponse(
stream,
content_type="text/event-stream",
headers={
"x-vercel-ai-data-stream": "v1", # This header is used for Vercel AI streaming,
"X-Accel-Buffering": "no", # Prevent nginx buffering
},
)
@drf.decorators.action(
detail=True,
methods=["post"],
@@ -2103,6 +2243,7 @@ class DocumentAccessViewSet(
"user__full_name",
"user__email",
"user__language",
"user__is_first_connection",
"document__id",
"document__path",
"document__depth",
@@ -2402,6 +2543,12 @@ class DocumentAskForAccessViewSet(
"""Create a document ask for access resource."""
document = self.get_document_or_404()
if document.get_role(request.user) in models.PRIVILEGED_ROLES:
return drf.response.Response(
{"detail": "You already have privileged access to this document."},
status=drf.status.HTTP_400_BAD_REQUEST,
)
serializer = serializers.DocumentAskForAccessCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
@@ -2458,7 +2605,10 @@ class ConfigView(drf.views.APIView):
Return a dictionary of public settings.
"""
array_settings = [
"AI_BOT",
"AI_FEATURE_ENABLED",
"AI_FEATURE_BLOCKNOTE_ENABLED",
"AI_FEATURE_LEGACY_ENABLED",
"API_USERS_SEARCH_QUERY_MIN_LENGTH",
"COLLABORATION_WS_URL",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",

View File

@@ -19,3 +19,21 @@ class ForceSessionMiddleware:
response = self.get_response(request)
return response
class SaveRawBodyMiddleware:
"""
Save the raw request body to use it later.
"""
def __init__(self, get_response):
"""Initialize the middleware."""
self.get_response = get_response
def __call__(self, request):
"""Save the raw request body in the request to use it later."""
if request.path.endswith(("/ai-proxy/", "/ai-proxy")):
request.raw_body = request.body
response = self.get_response(request)
return response

View File

@@ -22,7 +22,7 @@ def set_path_on_existing_documents(apps, schema_editor):
# 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)
numconv = NumConv(ALPHABET)
updates = []
for i, pk in enumerate(documents):

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.2.11 on 2026-03-04 14:49
from django.db import migrations, models
def set_is_first_connection_false(apps, schema_editor):
"""Update all existing user.is_first_connection to False."""
user = apps.get_model("core", "User")
user.objects.update(is_first_connection=False)
class Migration(migrations.Migration):
dependencies = [
("core", "0029_userreconciliationcsvimport_userreconciliation"),
]
operations = [
migrations.AddField(
model_name="user",
name="is_first_connection",
field=models.BooleanField(
default=True,
help_text="Whether the user has completed the first connection process.",
verbose_name="first connection status",
),
),
migrations.RunPython(
set_is_first_connection_false,
reverse_code=migrations.RunPython.noop,
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.2.12 on 2026-03-11 17:16
from django.conf import settings
from django.db import migrations
from core.models import PRIVILEGED_ROLES
def clean_onboarding_accesses(apps, schema_editor):
"""clean accesses on on-boarding documents."""
onboarding_document_ids = settings.USER_ONBOARDING_DOCUMENTS
if not onboarding_document_ids:
return
onboarding_document_ids = set(settings.USER_ONBOARDING_DOCUMENTS)
DocumentAccess = apps.get_model("core", "DocumentAccess")
DocumentAccess.objects.filter(document_id__in=onboarding_document_ids).exclude(
role__in=PRIVILEGED_ROLES
).delete()
class Migration(migrations.Migration):
dependencies = [
("core", "0030_user_is_first_connection"),
]
operations = [
migrations.RunPython(
clean_onboarding_accesses,
reverse_code=migrations.RunPython.noop,
),
]

View File

@@ -19,7 +19,7 @@ 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, transaction
from django.db import connection, models, transaction
from django.db.models.functions import Left, Length
from django.template.loader import render_to_string
from django.utils import timezone
@@ -193,6 +193,11 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
"Unselect this instead of deleting accounts."
),
)
is_first_connection = models.BooleanField(
_("first connection status"),
default=True,
help_text=_("Whether the user has completed the first connection process."),
)
objects = UserManager()
@@ -209,14 +214,90 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
def save(self, *args, **kwargs):
"""
If it's a new user, give its user access to the documents to which s.he was invited.
If it's a new user, give its user access to the documents they were invited to.
"""
is_adding = self._state.adding
super().save(*args, **kwargs)
if is_adding:
self._handle_onboarding_documents_access()
self._duplicate_onboarding_sandbox_document()
self._convert_valid_invitations()
def _handle_onboarding_documents_access(self):
"""
If the user is new and there are documents configured to be given to new users,
create link traces to these documents and pin them as favorites for the user.
"""
if settings.USER_ONBOARDING_DOCUMENTS:
onboarding_document_ids = set(settings.USER_ONBOARDING_DOCUMENTS)
onboarding_link_traces = []
favorite_documents = []
for document_id in onboarding_document_ids:
try:
document = Document.objects.get(id=document_id)
except Document.DoesNotExist:
logger.warning(
"Onboarding document with id %s does not exist. Skipping.",
document_id,
)
continue
if document.link_reach == LinkReachChoices.RESTRICTED:
logger.warning(
"Onboarding on a restricted document is not allowed. Must be public or "
"connected. Restricted document: %s",
document_id,
)
continue
onboarding_link_traces.append(LinkTrace(user=self, document=document))
favorite_documents.append(
DocumentFavorite(user=self, document_id=document_id)
)
LinkTrace.objects.bulk_create(onboarding_link_traces)
DocumentFavorite.objects.bulk_create(favorite_documents)
def _duplicate_onboarding_sandbox_document(self):
"""
If the user is new and there is a sandbox document configured,
duplicate the sandbox document for the user
"""
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
# transaction.atomic is used in a context manager to avoid a transaction if
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
with transaction.atomic():
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
try:
template_document = Document.objects.get(id=sandbox_id)
except Document.DoesNotExist:
logger.warning(
"Onboarding sandbox document with id %s does not exist. Skipping.",
sandbox_id,
)
return
sandbox_document = template_document.add_sibling(
"right",
title=template_document.title,
content=template_document.content,
attachments=template_document.attachments,
duplicated_from=template_document,
creator=self,
)
DocumentAccess.objects.create(
user=self, document=sandbox_document, role=RoleChoices.OWNER
)
def _convert_valid_invitations(self):
"""
Convert valid invitations to document accesses.
@@ -1219,6 +1300,7 @@ class Document(MP_Node, BaseModel):
return {
"accesses_manage": is_owner_or_admin,
"accesses_view": has_access_role,
"ai_proxy": ai_access,
"ai_transform": ai_access,
"ai_translate": ai_access,
"attachment_upload": can_update,
@@ -1260,7 +1342,7 @@ class Document(MP_Node, BaseModel):
"brandname": settings.EMAIL_BRAND_NAME,
"document": self,
"domain": domain,
"link": f"{domain}/docs/{self.id}/",
"link": f"{domain}/docs/{self.id}/?utm_source=docssharelink&utm_campaign={self.id}",
"link_label": self.title or str(_("Untitled Document")),
"button_label": _("Open"),
"logo_img": settings.EMAIL_LOGO_IMG,

View File

@@ -1,15 +1,68 @@
"""AI services."""
import asyncio
import json
import logging
import os
import queue
import threading
from collections.abc import AsyncIterator, Iterator
from typing import Any, Dict, Union
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from langfuse import get_client
from langfuse.openai import OpenAI as OpenAI_Langfuse
from pydantic_ai import Agent, DeferredToolRequests
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.tools import ToolDefinition
from pydantic_ai.toolsets.external import ExternalToolset
from pydantic_ai.ui import SSE_CONTENT_TYPE
from pydantic_ai.ui.vercel_ai import VercelAIAdapter
from pydantic_ai.ui.vercel_ai.request_types import RequestData, TextUIPart, UIMessage
from rest_framework.request import Request
from core import enums
if settings.LANGFUSE_PUBLIC_KEY:
from langfuse.openai import OpenAI
OpenAI = OpenAI_Langfuse
else:
from openai import OpenAI
log = logging.getLogger(__name__)
BLOCKNOTE_TOOL_STRICT_PROMPT = """
You are editing a BlockNote document via the tool applyDocumentOperations.
You MUST respond ONLY by calling applyDocumentOperations.
The tool input MUST be valid JSON:
{ "operations": [ ... ] }
Each operation MUST include "type" and it MUST be one of:
- "update" (requires: id, block)
- "add" (requires: referenceId, position, blocks)
- "delete" (requires: id)
VALID SHAPES (FOLLOW EXACTLY):
Update:
{ "type":"update", "id":"<id$>", "block":"<p>...</p>" }
IMPORTANT: "block" MUST be a STRING containing a SINGLE valid HTML element.
Add:
{ "type":"add", "referenceId":"<id$>", "position":"before|after", "blocks":["<p>...</p>"] }
IMPORTANT: "blocks" MUST be an ARRAY OF STRINGS.
Each item MUST be a STRING containing a SINGLE valid HTML element.
Delete:
{ "type":"delete", "id":"<id$>" }
IDs ALWAYS end with "$". Use ids EXACTLY as provided.
Return ONLY the JSON tool input. No prose, no markdown.
"""
AI_ACTIONS = {
"prompt": (
@@ -56,6 +109,40 @@ AI_TRANSLATE = (
)
def convert_async_generator_to_sync(async_gen: AsyncIterator[str]) -> Iterator[str]:
"""Convert an async generator to a sync generator."""
q: queue.Queue[str | object] = queue.Queue()
sentinel = object()
exc_sentinel = object()
async def run_async_gen():
try:
async for async_item in async_gen:
q.put(async_item)
except Exception as exc: # pylint: disable=broad-except #noqa: BLE001
q.put((exc_sentinel, exc))
finally:
q.put(sentinel)
def start_async_loop():
asyncio.run(run_async_gen())
thread = threading.Thread(target=start_async_loop, daemon=True)
thread.start()
try:
while True:
item = q.get()
if item is sentinel:
break
if isinstance(item, tuple) and item[0] is exc_sentinel:
# re-raise the exception in the sync context
raise item[1]
yield item
finally:
thread.join()
class AIService:
"""Service class for AI-related operations."""
@@ -96,3 +183,198 @@ class AIService:
language_display = enums.ALL_LANGUAGES.get(language, language)
system_content = AI_TRANSLATE.format(language=language_display)
return self.call_ai_api(system_content, text)
@staticmethod
def inject_document_state_messages(
messages: list[UIMessage],
) -> list[UIMessage]:
"""Inject document state context before user messages.
Port of BlockNote's injectDocumentStateMessages.
For each user message carrying documentState metadata, an assistant
message describing the current document/selection state is prepended
so the LLM sees it as context.
"""
result: list[UIMessage] = []
for message in messages:
if (
message.role == "user"
and isinstance(message.metadata, dict)
and "documentState" in message.metadata
):
doc_state = message.metadata["documentState"]
selection = doc_state.get("selection")
blocks = doc_state.get("blocks")
if selection:
parts = [
TextUIPart(
text=(
"This is the latest state of the selection "
"(ignore previous selections, you MUST issue "
"operations against this latest version of "
"the selection):"
),
),
TextUIPart(
text=json.dumps(doc_state.get("selectedBlocks")),
),
TextUIPart(
text=(
"This is the latest state of the entire "
"document (INCLUDING the selected text), you "
"can use this to find the selected text to "
"understand the context (but you MUST NOT "
"issue operations against this document, you "
"MUST issue operations against the selection):"
),
),
TextUIPart(text=json.dumps(blocks)),
]
else:
text = (
"There is no active selection. This is the latest "
"state of the document (ignore previous documents, "
"you MUST issue operations against this latest "
"version of the document). The cursor is BETWEEN "
"two blocks as indicated by cursor: true."
)
if doc_state.get("isEmptyDocument"):
text += (
"Because the document is empty, YOU MUST first "
"update the empty block before adding new blocks."
)
else:
text += (
"Prefer updating existing blocks over removing "
"and adding (but this also depends on the "
"user's question)."
)
parts = [
TextUIPart(text=text),
TextUIPart(text=json.dumps(blocks)),
]
result.append(
UIMessage(
role="assistant",
id=f"assistant-document-state-{message.id}",
parts=parts,
)
)
result.append(message)
return result
@staticmethod
def tool_definitions_to_toolset(
tool_definitions: Dict[str, Any],
) -> ExternalToolset:
"""Convert serialized tool definitions to a pydantic-ai ExternalToolset.
Port of BlockNote's toolDefinitionsToToolSet.
Builds ToolDefinition objects from the JSON-Schema-based definitions
sent by the frontend and wraps them in an ExternalToolset so that
pydantic-ai advertises them to the LLM without trying to execute them
server-side (execution is deferred to the frontend).
"""
tool_defs = [
ToolDefinition(
name=name,
description=defn.get("description", ""),
parameters_json_schema=defn.get("inputSchema", {}),
kind="external",
metadata={
"output_schema": defn.get("outputSchema"),
},
)
for name, defn in tool_definitions.items()
]
return ExternalToolset(tool_defs)
def _harden_messages(
self, run_input: RequestData, tool_definitions: Dict[str, Any]
):
"""
Harden messages if applyDocumentOperations tool is used.
We would like the system_prompt property in the Agent initialization
but for UI adapter, like vercel, the agent is ignoring it
see https://github.com/pydantic/pydantic-ai/issues/3315
We have to inject it in the run_input.messages if needed.
"""
for name, _defn in tool_definitions.items():
if name == "applyDocumentOperations":
run_input.messages.insert(
0,
UIMessage(
id="system-force-tool-usage",
role="system",
parts=[TextUIPart(text=BLOCKNOTE_TOOL_STRICT_PROMPT)],
),
)
return
def _build_async_stream(self, request: Request) -> AsyncIterator[str]:
"""Build the async stream from the AI provider."""
instrument_enabled = settings.LANGFUSE_PUBLIC_KEY is not None
if instrument_enabled:
langfuse = get_client()
langfuse.auth_check()
Agent.instrument_all()
model = OpenAIChatModel(
settings.AI_MODEL,
provider=OpenAIProvider(
base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY
),
)
agent = Agent(model, instrument=instrument_enabled)
accept = request.META.get("HTTP_ACCEPT", SSE_CONTENT_TYPE)
run_input = VercelAIAdapter.build_run_input(request.raw_body)
# Inject document state context into the conversation
run_input.messages = self.inject_document_state_messages(run_input.messages)
# Build an ExternalToolset from frontend-supplied tool definitions
raw_tool_defs = (
run_input.model_extra.get("toolDefinitions")
if run_input.model_extra
else None
)
toolset = (
self.tool_definitions_to_toolset(raw_tool_defs) if raw_tool_defs else None
)
if raw_tool_defs:
self._harden_messages(run_input, raw_tool_defs)
adapter = VercelAIAdapter(
agent=agent,
run_input=run_input,
accept=accept,
sdk_version=settings.AI_VERCEL_SDK_VERSION,
)
event_stream = adapter.run_stream(
output_type=[str, DeferredToolRequests] if toolset else None,
toolsets=[toolset] if toolset else None,
)
return adapter.encode_stream(event_stream)
def stream(self, request: Request) -> Union[AsyncIterator[str], Iterator[str]]:
"""Stream AI API requests to the configured AI provider.
Returns an async iterator when running in async mode (ASGI)
or a sync iterator when running in sync mode (WSGI).
"""
async_stream = self._build_async_stream(request)
if os.environ.get("PYTHON_SERVER_MODE", "sync") == "async":
return async_stream
return convert_async_generator_to_sync(async_stream)

View File

@@ -245,15 +245,18 @@ def test_api_document_accesses_list_authenticated_related_privileged(
"path": access.document.path,
"depth": access.document.depth,
},
"user": {
"id": str(access.user.id),
"email": access.user.email,
"language": access.user.language,
"full_name": access.user.full_name,
"short_name": access.user.short_name,
}
if access.user
else None,
"user": (
{
"id": str(access.user.id),
"email": access.user.email,
"language": access.user.language,
"full_name": access.user.full_name,
"short_name": access.user.short_name,
"is_first_connection": access.user.is_first_connection,
}
if access.user
else None
),
"max_ancestors_role": None,
"max_role": access.role,
"team": access.team,

View File

@@ -0,0 +1,387 @@
"""
Test AI proxy API endpoint for users in impress's core app.
"""
import random
from unittest.mock import patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.AI_BASE_URL = "http://localhost-ai:12345/"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_proxy_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI proxy if the link reach
and role don't allow it.
"""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = APIClient().post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
},
format="json",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_anonymous_success(mock_stream):
"""
Anonymous users should be able to request AI proxy to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_stream.return_value = iter(["data: chunk1\n", "data: chunk2\n"])
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = APIClient().post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 200
assert response["Content-Type"] == "text/event-stream"
assert response["x-vercel-ai-data-stream"] == "v1"
assert response["X-Accel-Buffering"] == "no"
content = b"".join(response.streaming_content).decode()
assert "chunk1" in content
assert "chunk2" in content
mock_stream.assert_called_once()
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
def test_api_documents_ai_proxy_anonymous_limited_by_setting():
"""
Anonymous users should not be able to request AI proxy to a document
if AI_ALLOW_REACH_FROM setting restricts it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = APIClient().post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("public", "reader"),
],
)
def test_api_documents_ai_proxy_authenticated_forbidden(reach, role):
"""
Users who are not related to a document can't request AI proxy if the
link reach and role don't allow it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 403
@pytest.mark.parametrize(
"reach, role",
[
("authenticated", "editor"),
("public", "editor"),
],
)
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_authenticated_success(mock_stream, reach, role):
"""
Authenticated users should be able to request AI proxy to a document
if the link reach and role permit it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
mock_stream.return_value = iter(["data: response\n"])
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 200
assert response["Content-Type"] == "text/event-stream"
mock_stream.assert_called_once()
@pytest.mark.parametrize("via", VIA)
def test_api_documents_ai_proxy_reader(via, mock_user_teams):
"""Users with reader access should not be able to request AI proxy."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 403
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_success(mock_stream, via, role, mock_user_teams):
"""Users with sufficient permissions should be able to request AI proxy."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
if via == 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", role=role
)
mock_stream.return_value = iter(["data: success\n"])
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 200
assert response["Content-Type"] == "text/event-stream"
assert response["x-vercel-ai-data-stream"] == "v1"
assert response["X-Accel-Buffering"] == "no"
content = b"".join(response.streaming_content).decode()
assert "success" in content
mock_stream.assert_called_once()
@pytest.mark.parametrize(
"setting_to_disable", ["AI_FEATURE_ENABLED", "AI_FEATURE_BLOCKNOTE_ENABLED"]
)
def test_api_documents_ai_proxy_ai_feature_disabled(settings, setting_to_disable):
"""When AI_FEATURE_ENABLED is False, the endpoint returns 400."""
setattr(settings, setting_to_disable, False)
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
response = client.post(
f"/api/v1.0/documents/{document.id!s}/ai-proxy/",
b"{}",
content_type="application/json",
)
assert response.status_code == 400
assert response.json() == ["AI feature is not enabled."]
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_throttling_document(mock_stream):
"""
Throttling per document should be triggered on the AI proxy endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_stream.return_value = iter(["data: ok\n"])
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
for _ in range(3):
mock_stream.return_value = iter(["data: ok\n"])
user = factories.UserFactory()
client.force_login(user)
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 200
user = factories.UserFactory()
client.force_login(user)
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_throttling_user(mock_stream):
"""
Throttling per user should be triggered on the AI proxy endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
for _ in range(3):
mock_stream.return_value = iter(["data: ok\n"])
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 200
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}
@patch("core.services.ai_services.AIService.stream")
def test_api_documents_ai_proxy_returns_streaming_response(mock_stream):
"""AI proxy should return a StreamingHttpResponse with correct headers."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_stream.return_value = iter(["data: part1\n", "data: part2\n", "data: part3\n"])
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
b"{}",
content_type="application/json",
)
assert response.status_code == 200
assert response["Content-Type"] == "text/event-stream"
assert response["x-vercel-ai-data-stream"] == "v1"
assert response["X-Accel-Buffering"] == "no"
chunks = list(response.streaming_content)
assert len(chunks) == 3
def test_api_documents_ai_proxy_invalid_payload():
"""AI Proxy should return a 400 if the payload is invalid."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/ai-proxy/",
b'{"foo": "bar", "trigger": "submit-message"}',
content_type="application/json",
)
assert response.status_code == 400
assert response.json() == {"detail": "Invalid submitted payload"}

View File

@@ -9,6 +9,7 @@ import pytest
from rest_framework.test import APIClient
from core.api.serializers import UserSerializer
from core.choices import PRIVILEGED_ROLES
from core.factories import (
DocumentAskForAccessFactory,
DocumentFactory,
@@ -199,6 +200,27 @@ def test_api_documents_ask_for_access_create_authenticated_already_has_ask_for_a
assert response.json() == {"detail": "You already ask to access to this document."}
@pytest.mark.parametrize("role", PRIVILEGED_ROLES)
def test_api_documents_ask_for_access_create_authenticated_already_has_privileged_access(
role,
):
"""
Authenticated users with privileged access (owner or admin) should not be able to
create a document ask for access.
"""
user = UserFactory()
document = DocumentFactory(users=[(user, role)])
client = APIClient()
client.force_login(user)
response = client.post(f"/api/v1.0/documents/{document.id}/ask-for-access/")
assert response.status_code == 400
assert response.json() == {
"detail": "You already have privileged access to this document."
}
## List

View File

@@ -318,3 +318,424 @@ def test_api_documents_duplicate_reader_non_root_document():
assert duplicated_document.is_root()
assert duplicated_document.accesses.count() == 1
assert duplicated_document.accesses.get(user=user).role == "owner"
def test_api_documents_duplicate_with_descendants_simple():
"""
Duplicating a document with descendants flag should recursively duplicate all children.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Create document tree
root = factories.DocumentFactory(
users=[(user, "owner")],
title="Root Document",
)
child1 = factories.DocumentFactory(
parent=root,
title="Child 1",
)
child2 = factories.DocumentFactory(
parent=root,
title="Child 2",
)
initial_count = models.Document.objects.count()
assert initial_count == 3
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
# Check that all documents were duplicated (6 total: 3 original + 3 duplicated)
assert models.Document.objects.count() == 6
# Check root duplication
assert duplicated_root.title == "Copy of Root Document"
assert duplicated_root.creator == user
assert duplicated_root.duplicated_from == root
assert duplicated_root.get_children().count() == 2
# Check children duplication
duplicated_children = duplicated_root.get_children().order_by("title")
assert duplicated_children.count() == 2
duplicated_child1 = duplicated_children.first()
assert duplicated_child1.title == "Copy of Child 1"
assert duplicated_child1.creator == user
assert duplicated_child1.duplicated_from == child1
assert duplicated_child1.get_parent() == duplicated_root
duplicated_child2 = duplicated_children.last()
assert duplicated_child2.title == "Copy of Child 2"
assert duplicated_child2.creator == user
assert duplicated_child2.duplicated_from == child2
assert duplicated_child2.get_parent() == duplicated_root
def test_api_documents_duplicate_with_descendants_multi_level():
"""
Duplicating should recursively handle multiple levels of nesting.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
root = factories.DocumentFactory(
users=[(user, "owner")],
title="Level 0",
)
child = factories.DocumentFactory(
parent=root,
title="Level 1",
)
grandchild = factories.DocumentFactory(
parent=child,
title="Level 2",
)
great_grandchild = factories.DocumentFactory(
parent=grandchild,
title="Level 3",
)
initial_count = models.Document.objects.count()
assert initial_count == 4
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
# Check that all documents were duplicated
assert models.Document.objects.count() == 8
# Verify the tree structure
assert duplicated_root.depth == root.depth
dup_children = duplicated_root.get_children()
assert dup_children.count() == 1
dup_child = dup_children.first()
assert dup_child.title == "Copy of Level 1"
assert dup_child.duplicated_from == child
dup_grandchildren = dup_child.get_children()
assert dup_grandchildren.count() == 1
dup_grandchild = dup_grandchildren.first()
assert dup_grandchild.title == "Copy of Level 2"
assert dup_grandchild.duplicated_from == grandchild
dup_great_grandchildren = dup_grandchild.get_children()
assert dup_great_grandchildren.count() == 1
dup_great_grandchild = dup_great_grandchildren.first()
assert dup_great_grandchild.title == "Copy of Level 3"
assert dup_great_grandchild.duplicated_from == great_grandchild
def test_api_documents_duplicate_with_descendants_and_attachments():
"""
Duplicating with descendants should properly handle attachments in all children.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Create documents with attachments
root_id = uuid.uuid4()
child_id = uuid.uuid4()
image_key_root, image_url_root = get_image_refs(root_id)
image_key_child, image_url_child = get_image_refs(child_id)
# Create root document with attachment
ydoc = pycrdt.Doc()
fragment = pycrdt.XmlFragment(
[
pycrdt.XmlElement("img", {"src": image_url_root}),
]
)
ydoc["document-store"] = fragment
update = ydoc.get_update()
root_content = base64.b64encode(update).decode("utf-8")
root = factories.DocumentFactory(
id=root_id,
users=[(user, "owner")],
title="Root with Image",
content=root_content,
attachments=[image_key_root],
)
# Create child with different attachment
ydoc_child = pycrdt.Doc()
fragment_child = pycrdt.XmlFragment(
[
pycrdt.XmlElement("img", {"src": image_url_child}),
]
)
ydoc_child["document-store"] = fragment_child
update_child = ydoc_child.get_update()
child_content = base64.b64encode(update_child).decode("utf-8")
child = factories.DocumentFactory(
id=child_id,
parent=root,
title="Child with Image",
content=child_content,
attachments=[image_key_child],
)
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
# Check root attachments
assert duplicated_root.attachments == [image_key_root]
assert duplicated_root.content == root_content
# Check child attachments
dup_children = duplicated_root.get_children()
assert dup_children.count() == 1
dup_child = dup_children.first()
assert dup_child.attachments == [image_key_child]
assert dup_child.content == child_content
def test_api_documents_duplicate_with_descendants_and_accesses():
"""
Duplicating with descendants and accesses should propagate accesses to all children.
"""
user = factories.UserFactory()
other_user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Create document tree with accesses
root = factories.DocumentFactory(
users=[(user, "owner"), (other_user, "editor")],
title="Root",
)
child = factories.DocumentFactory(
parent=root,
title="Child",
)
factories.UserDocumentAccessFactory(document=child, user=other_user, role="reader")
# Duplicate with descendants and accesses
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True, "with_accesses": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
# Check root accesses (should be duplicated)
root_accesses = duplicated_root.accesses.order_by("user_id")
assert root_accesses.count() == 2
assert root_accesses.get(user=user).role == "owner"
assert root_accesses.get(user=other_user).role == "editor"
# Check child accesses (should be duplicated)
dup_children = duplicated_root.get_children()
dup_child = dup_children.first()
child_accesses = dup_child.accesses.order_by("user_id")
assert child_accesses.count() == 1
assert child_accesses.get(user=other_user).role == "reader"
@pytest.mark.parametrize("role", ["editor", "reader"])
def test_api_documents_duplicate_with_descendants_non_root_document_becomes_root(role):
"""
When duplicating a non-root document with descendants as a reader/editor,
it should become a root document and still duplicate its children.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent = factories.DocumentFactory(users=[(user, "owner")])
child = factories.DocumentFactory(
parent=parent,
users=[(user, role)],
title="Sub Document",
)
grandchild = factories.DocumentFactory(
parent=child,
title="Grandchild",
)
assert child.is_child_of(parent)
# Duplicate the child (non-root) with descendants
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_child = models.Document.objects.get(id=response.json()["id"])
assert duplicated_child.title == "Copy of Sub Document"
dup_grandchildren = duplicated_child.get_children()
assert dup_grandchildren.count() == 1
dup_grandchild = dup_grandchildren.first()
assert dup_grandchild.title == "Copy of Grandchild"
assert dup_grandchild.duplicated_from == grandchild
def test_api_documents_duplicate_without_descendants_should_not_duplicate_children():
"""
When with_descendants is not set or False, children should not be duplicated.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Create document tree
root = factories.DocumentFactory(
users=[(user, "owner")],
title="Root",
)
child = factories.DocumentFactory(
parent=root,
title="Child",
)
initial_count = models.Document.objects.count()
assert initial_count == 2
# Duplicate without descendants (default behavior)
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
# Only root should be duplicated, not children
assert models.Document.objects.count() == 3
assert duplicated_root.get_children().count() == 0
def test_api_documents_duplicate_with_descendants_preserves_link_configuration():
"""
Duplicating with descendants should preserve link configuration (link_reach, link_role)
for all children when with_accesses is True.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Create document tree with specific link configurations
root = factories.DocumentFactory(
users=[(user, "owner")],
title="Root",
link_reach="public",
link_role="reader",
)
child = factories.DocumentFactory(
parent=root,
title="Child",
link_reach="restricted",
link_role="editor",
)
# Duplicate with descendants and accesses
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True, "with_accesses": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
# Check root link configuration
assert duplicated_root.link_reach == root.link_reach
assert duplicated_root.link_role == root.link_role
# Check child link configuration
dup_children = duplicated_root.get_children()
dup_child = dup_children.first()
assert dup_child.link_reach == child.link_reach
assert dup_child.link_role == child.link_role
def test_api_documents_duplicate_with_descendants_complex_tree():
"""
Test duplication of a complex tree structure with multiple branches.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# Create a complex tree:
# root
# / \
# c1 c2
# / \ \
# gc1 gc2 gc3
root = factories.DocumentFactory(
users=[(user, "owner")],
title="Root",
)
child1 = factories.DocumentFactory(parent=root, title="Child 1")
child2 = factories.DocumentFactory(parent=root, title="Child 2")
_grandchild1 = factories.DocumentFactory(parent=child1, title="GrandChild 1")
_grandchild2 = factories.DocumentFactory(parent=child1, title="GrandChild 2")
_grandchild3 = factories.DocumentFactory(parent=child2, title="GrandChild 3")
initial_count = models.Document.objects.count()
assert initial_count == 6
# Duplicate with descendants
response = client.post(
f"/api/v1.0/documents/{root.id!s}/duplicate/",
{"with_descendants": True},
format="json",
)
assert response.status_code == 201
duplicated_root = models.Document.objects.get(id=response.json()["id"])
# All documents should be duplicated
assert models.Document.objects.count() == 12
# Check structure is preserved
dup_children = duplicated_root.get_children().order_by("title")
assert dup_children.count() == 2
dup_child1 = dup_children.first()
assert dup_child1.title == "Copy of Child 1"
dup_grandchildren1 = dup_child1.get_children().order_by("title")
assert dup_grandchildren1.count() == 2
assert dup_grandchildren1.first().title == "Copy of GrandChild 1"
assert dup_grandchildren1.last().title == "Copy of GrandChild 2"
dup_child2 = dup_children.last()
assert dup_child2.title == "Copy of Child 2"
dup_grandchildren2 = dup_child2.get_children()
assert dup_grandchildren2.count() == 1
assert dup_grandchildren2.first().title == "Copy of GrandChild 3"

View File

@@ -29,6 +29,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": document.link_role == "editor",
@@ -107,6 +108,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": grand_parent.link_role == "editor",
@@ -215,6 +217,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": document.link_role == "editor",
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"attachment_upload": document.link_role == "editor",
@@ -300,6 +303,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": grand_parent.link_role == "editor",
"ai_transform": grand_parent.link_role == "editor",
"ai_translate": grand_parent.link_role == "editor",
"attachment_upload": grand_parent.link_role == "editor",
@@ -498,6 +502,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"abilities": {
"accesses_manage": access.role in ["administrator", "owner"],
"accesses_view": True,
"ai_proxy": access.role not in ["reader", "commenter"],
"ai_transform": access.role not in ["reader", "commenter"],
"ai_translate": access.role not in ["reader", "commenter"],
"attachment_upload": access.role not in ["reader", "commenter"],
@@ -1057,3 +1062,48 @@ def test_api_documents_retrieve_permanently_deleted_related(role, depth):
assert response.status_code == 404
assert response.json() == {"detail": "Not found."}
def test_api_documents_retrieve_without_content():
"""
Test retrieve using without_content query string should remove the content in the response
"""
user = factories.UserFactory()
document = factories.DocumentFactory(creator=user, users=[(user, "owner")])
client = APIClient()
client.force_login(user)
with mock.patch("core.models.Document.content") as mock_document_content:
response = client.get(
f"/api/v1.0/documents/{document.id!s}/?without_content=true"
)
assert response.status_code == 200
payload = response.json()
assert "content" not in payload
mock_document_content.assert_not_called()
def test_api_documents_retrieve_without_content_invalid_value():
"""
Test retrieve using without_content query string but an invalid value
should return a 400
"""
user = factories.UserFactory()
document = factories.DocumentFactory(creator=user, users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.get(
f"/api/v1.0/documents/{document.id!s}/?without_content=invalid-value"
)
assert response.status_code == 400
assert response.json() == ["Must be a valid boolean."]

View File

@@ -72,6 +72,7 @@ def test_api_documents_trashbin_format():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,

View File

@@ -1,7 +1,5 @@
import pytest
from core import models
@pytest.mark.django_db
def test_update_blank_title_migration(migrator):

View File

@@ -7,8 +7,6 @@ from django.core.files.storage import default_storage
import pycrdt
import pytest
from core import models
@pytest.mark.django_db
def test_populate_attachments_on_all_documents(migrator):

View File

@@ -0,0 +1,52 @@
"""Module testing migration 0030 about adding is_first_connection to user model."""
from django.contrib.auth.hashers import make_password
import factory
import pytest
from core import models
@pytest.mark.django_db
def test_set_is_first_connection_false(migrator):
"""
Test that once the migration adding is_first_connection column to user model is applied
all existing user have the False value.
"""
old_state = migrator.apply_initial_migration(
("core", "0029_userreconciliationcsvimport_userreconciliation")
)
OldUser = old_state.apps.get_model("core", "User")
old_user1 = OldUser.objects.create(
email="email1@example.com", sub="user1", password=make_password("password")
)
old_user2 = OldUser.objects.create(
email="email2@example.com", sub="user2", password=make_password("password")
)
assert hasattr(old_user1, "is_first_connection") is False
assert hasattr(old_user2, "is_first_connection") is False
# # Apply the migration
new_state = migrator.apply_tested_migration(
("core", "0030_user_is_first_connection")
)
NewUser = new_state.apps.get_model("core", "User")
updated_user1 = NewUser.objects.get(id=old_user1.id)
assert updated_user1.is_first_connection is False
updated_user2 = NewUser.objects.get(id=old_user2.id)
assert updated_user2.is_first_connection is False
# create a new user after migration
new_user1 = NewUser.objects.create(
email="email3example.com", sub="user3", password=make_password("password")
)
assert new_user1.is_first_connection is True

View File

@@ -0,0 +1,193 @@
"""Module testing migration 0031_clean_onboarding_accesses."""
from django.contrib.auth.hashers import make_password
import pytest
def create_user(OldUser, n):
"""Create a user with a unique sub and email based on the given index."""
return OldUser.objects.create(
email=f"user-{n}@example.com",
sub=f"user-{n}",
password=make_password("password"),
)
@pytest.mark.django_db
def test_clean_onboarding_accesses(migrator, settings):
"""Test migration 0031_clean_onboarding_accesses."""
old_state = migrator.apply_initial_migration(
("core", "0030_user_is_first_connection")
)
OldUser = old_state.apps.get_model("core", "User")
OldDocument = old_state.apps.get_model("core", "Document")
OldDocumentAccess = old_state.apps.get_model("core", "DocumentAccess")
# Create onboarding documents
onboarding_doc_1 = OldDocument.objects.create(
title="Onboarding Doc 1", depth=1, path="0000001", link_reach="public"
)
onboarding_doc_2 = OldDocument.objects.create(
title="Onboarding Doc 2", depth=1, path="0000002", link_reach="public"
)
onboarding_documents = [onboarding_doc_1, onboarding_doc_2]
settings.USER_ONBOARDING_DOCUMENTS = [str(doc.id) for doc in onboarding_documents]
# Create other documents
non_onboarding_doc_1 = OldDocument.objects.create(
title="Non-Onboarding Doc 1", depth=1, path="0000003", link_reach="public"
)
non_onboarding_doc_2 = OldDocument.objects.create(
title="Non-Onboarding Doc 2", depth=1, path="0000004", link_reach="public"
)
non_onboarding_doc_3 = OldDocument.objects.create(
title="Non-Onboarding Doc 3", depth=1, path="0000005", link_reach="public"
)
non_onboarding_documents = [
non_onboarding_doc_1,
non_onboarding_doc_2,
non_onboarding_doc_3,
]
all_documents = onboarding_documents + non_onboarding_documents
user_counter = 0
# For every document create privileged roles: owner and admin
for document in all_documents:
OldDocumentAccess.objects.create(
document=document,
user=create_user(OldUser, user_counter),
role="owner",
)
user_counter += 1
OldDocumentAccess.objects.create(
document=document,
user=create_user(OldUser, user_counter),
role="administrator",
)
user_counter += 1
# For every document, create non-privileged roles
for document in all_documents:
for role in ["reader", "editor", "commenter"]:
for _ in range(10):
OldDocumentAccess.objects.create(
document=document,
user=create_user(OldUser, user_counter),
role=role,
)
user_counter += 1
onboarding_ids = [doc.id for doc in onboarding_documents]
non_onboarding_ids = [doc.id for doc in non_onboarding_documents]
# All documents should have 32 accesses each, so 160 accesses total
assert OldDocumentAccess.objects.count() == 160
assert (
OldDocumentAccess.objects.filter(document_id__in=onboarding_ids)
.exclude(role__in=["administrator", "owner"])
.count()
== 60
)
assert (
OldDocumentAccess.objects.filter(
document_id__in=onboarding_ids, role__in=["administrator", "owner"]
).count()
== 4
)
assert (
OldDocumentAccess.objects.filter(document_id__in=non_onboarding_ids)
.exclude(role__in=["administrator", "owner"])
.count()
== 90
)
assert (
OldDocumentAccess.objects.filter(
document_id__in=non_onboarding_ids, role__in=["administrator", "owner"]
).count()
== 6
)
# Apply the migration
new_state = migrator.apply_tested_migration(
("core", "0031_clean_onboarding_accesses")
)
NewDocumentAccess = new_state.apps.get_model("core", "DocumentAccess")
# 60 accesses should have been removed (30 non-privileged for each onboarding doc)
assert NewDocumentAccess.objects.count() == 100
# Non-privileged roles should have been deleted on the onboarding documents
assert (
NewDocumentAccess.objects.filter(document_id__in=onboarding_ids)
.exclude(role__in=["administrator", "owner"])
.count()
== 0
)
# Privileged roles should have been kept
assert (
NewDocumentAccess.objects.filter(
document_id__in=onboarding_ids, role__in=["administrator", "owner"]
).count()
== 4
)
# On other documents, all accesses should remain
assert (
NewDocumentAccess.objects.filter(document_id__in=non_onboarding_ids)
.exclude(role__in=["administrator", "owner"])
.count()
== 90
)
# Privileged roles should have been kept
assert (
NewDocumentAccess.objects.filter(
document_id__in=non_onboarding_ids, role__in=["administrator", "owner"]
).count()
== 6
)
@pytest.mark.django_db
def test_clean_onboarding_accesses_no_setting(migrator, settings):
"""Test migration 0031 does not delete any access when USER_ONBOARDING_DOCUMENTS is empty."""
old_state = migrator.apply_initial_migration(
("core", "0030_user_is_first_connection")
)
OldUser = old_state.apps.get_model("core", "User")
OldDocument = old_state.apps.get_model("core", "Document")
OldDocumentAccess = old_state.apps.get_model("core", "DocumentAccess")
settings.USER_ONBOARDING_DOCUMENTS = []
doc_1 = OldDocument.objects.create(title="Doc 1", depth=1, path="0000001")
doc_2 = OldDocument.objects.create(title="Doc 2", depth=1, path="0000002")
user_counter = 0
for document in [doc_1, doc_2]:
for role in ["owner", "administrator", "reader", "editor", "commenter"]:
OldDocumentAccess.objects.create(
document=document,
user=create_user(OldUser, user_counter),
role=role,
)
user_counter += 1
assert OldDocumentAccess.objects.count() == 10
new_state = migrator.apply_tested_migration(
("core", "0031_clean_onboarding_accesses")
)
NewDocumentAccess = new_state.apps.get_model("core", "DocumentAccess")
# No accesses should have been deleted
assert NewDocumentAccess.objects.count() == 10

View File

@@ -19,7 +19,10 @@ pytestmark = pytest.mark.django_db
@override_settings(
AI_BOT={"name": "Test Bot", "color": "#000000"},
AI_FEATURE_ENABLED=False,
AI_FEATURE_BLOCKNOTE_ENABLED=False,
AI_FEATURE_LEGACY_ENABLED=False,
API_USERS_SEARCH_QUERY_MIN_LENGTH=6,
COLLABORATION_WS_URL="http://testcollab/",
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
@@ -44,7 +47,10 @@ def test_api_config(is_authenticated):
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
assert response.json() == {
"AI_BOT": {"name": "Test Bot", "color": "#000000"},
"AI_FEATURE_ENABLED": False,
"AI_FEATURE_BLOCKNOTE_ENABLED": False,
"AI_FEATURE_LEGACY_ENABLED": False,
"API_USERS_SEARCH_QUERY_MIN_LENGTH": 6,
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,

View File

@@ -460,6 +460,7 @@ def test_api_users_retrieve_me_authenticated():
"full_name": user.full_name,
"language": user.language,
"short_name": user.short_name,
"is_first_connection": True,
}
@@ -489,9 +490,37 @@ def test_api_users_retrieve_me_authenticated_empty_name():
"full_name": "test_foo",
"language": user.language,
"short_name": "test_foo",
"is_first_connection": True,
}
def test_api_users_retrieve_me_onboarding():
"""
On first connection of a new user, the "is_first_connection" flag should be True.
The frontend can use this flag to trigger specific behavior for first time users,
e.g. showing an onboarding message, and update the flag to False after onboarding is done.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# First request: flag should be True
first_response = client.get("/api/v1.0/users/me/")
assert first_response.status_code == 200
assert first_response.json()["is_first_connection"] is True
update_response = client.post("/api/v1.0/users/onboarding-done/")
assert update_response.status_code == 200
# Second request: flag should be False
second_response = client.get("/api/v1.0/users/me/")
assert second_response.status_code == 200
assert second_response.json()["is_first_connection"] is False
def test_api_users_retrieve_anonymous():
"""Anonymous users should not be allowed to retrieve a user."""
client = APIClient()

View File

@@ -155,6 +155,7 @@ def test_models_documents_get_abilities_forbidden(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
@@ -220,6 +221,7 @@ def test_models_documents_get_abilities_reader(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
@@ -290,6 +292,7 @@ def test_models_documents_get_abilities_commenter(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
@@ -357,6 +360,7 @@ def test_models_documents_get_abilities_editor(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": is_authenticated,
"ai_transform": is_authenticated,
"ai_translate": is_authenticated,
"attachment_upload": True,
@@ -413,6 +417,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
expected_abilities = {
"accesses_manage": True,
"accesses_view": True,
"ai_proxy": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
@@ -455,6 +460,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
assert document.get_abilities(user) == {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
@@ -501,6 +507,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
expected_abilities = {
"accesses_manage": True,
"accesses_view": True,
"ai_proxy": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
@@ -557,6 +564,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
expected_abilities = {
"accesses_manage": False,
"accesses_view": True,
"ai_proxy": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
@@ -620,6 +628,7 @@ def test_models_documents_get_abilities_reader_user(
"accesses_view": True,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_proxy": access_from_link and ai_access_setting != "restricted",
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"attachment_upload": access_from_link,
@@ -686,6 +695,7 @@ def test_models_documents_get_abilities_commenter_user(
"accesses_view": True,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_proxy": access_from_link and ai_access_setting != "restricted",
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"attachment_upload": access_from_link,
@@ -747,6 +757,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
assert abilities == {
"accesses_manage": False,
"accesses_view": True,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
@@ -878,6 +889,7 @@ def test_models_document_get_abilities_ai_access_authenticated(is_authenticated,
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_proxy"] is True
assert abilities["ai_transform"] is True
assert abilities["ai_translate"] is True
@@ -897,6 +909,7 @@ def test_models_document_get_abilities_ai_access_public(is_authenticated, reach)
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_proxy"] == is_authenticated
assert abilities["ai_transform"] == is_authenticated
assert abilities["ai_translate"] == is_authenticated
@@ -1021,7 +1034,10 @@ def test_models_documents__email_invitation__success():
f"Test Sender (sender@example.com) invited you with the role &quot;editor&quot; "
f"on the following document: {document.title}" in email_content
)
assert f"docs/{document.id}/" in email_content
assert (
f"docs/{document.id}/?utm_source=docssharelink&amp;utm_campaign={document.id}"
in email_content
)
@pytest.mark.parametrize(
@@ -1051,10 +1067,18 @@ def test_models_documents__email_invitation__url_app_param(email_url_app):
# Determine expected domain
if email_url_app:
assert f"https://test-example.com/docs/{document.id}/" in email_content
expected_url = (
f"https://test-example.com/docs/{document.id}/"
f"?utm_source=docssharelink&amp;utm_campaign={document.id}"
)
assert expected_url in email_content
else:
# Default Site domain is example.com
assert f"example.com/docs/{document.id}/" in email_content
expected_url = (
f"example.com/docs/{document.id}/"
f"?utm_source=docssharelink&amp;utm_campaign={document.id}"
)
assert expected_url in email_content
def test_models_documents__email_invitation__success_empty_title():
@@ -1085,7 +1109,10 @@ def test_models_documents__email_invitation__success_empty_title():
"Test Sender (sender@example.com) invited you with the role &quot;editor&quot; "
"on the following document: Untitled Document" in email_content
)
assert f"docs/{document.id}/" in email_content
assert (
f"docs/{document.id}/?utm_source=docssharelink&amp;utm_campaign={document.id}"
in email_content
)
def test_models_documents__email_invitation__success_fr():
@@ -1120,7 +1147,10 @@ def test_models_documents__email_invitation__success_fr():
f"Test Sender2 (sender2@example.com) vous a invité avec le rôle &quot;propriétaire&quot; "
f"sur le document suivant : {document.title}" in email_content
)
assert f"docs/{document.id}/" in email_content
assert (
f"docs/{document.id}/?utm_source=docssharelink&amp;utm_campaign={document.id}"
in email_content
)
@mock.patch(

View File

@@ -79,7 +79,7 @@ def test_models_invitations_is_expired():
assert expired_invitation.is_expired is True
def test_models_invitationd_new_userd_convert_invitations_to_accesses():
def test_models_invitations_new_user_convert_invitations_to_accesses():
"""
Upon creating a new user, invitations linked to the email
should be converted to accesses and then deleted.
@@ -114,7 +114,7 @@ def test_models_invitationd_new_userd_convert_invitations_to_accesses():
).exists() # the other invitation remains
def test_models_invitationd_new_user_filter_expired_invitations():
def test_models_invitations_new_user_filter_expired_invitations():
"""
Upon creating a new identity, valid invitations should be converted into accesses
and expired invitations should remain unchanged.
@@ -145,7 +145,7 @@ def test_models_invitationd_new_user_filter_expired_invitations():
@pytest.mark.parametrize("num_invitations, num_queries", [(0, 3), (1, 7), (20, 7)])
def test_models_invitationd_new_userd_user_creation_constant_num_queries(
def test_models_invitations_new_userd_user_creation_constant_num_queries(
django_assert_num_queries, num_invitations, num_queries
):
"""

View File

@@ -2,7 +2,12 @@
Unit tests for the User model
"""
import uuid
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import patch
from django.core.exceptions import ValidationError
from django.test.utils import override_settings
import pytest
@@ -74,3 +79,253 @@ def test_modes_users_convert_valid_invitations():
id=invitation_other_document.id
).exists()
assert models.Invitation.objects.filter(id=other_email_invitation.id).exists()
@override_settings(USER_ONBOARDING_DOCUMENTS=[])
def test_models_users_handle_onboarding_documents_access_empty_setting():
"""
When USER_ONBOARDING_DOCUMENTS is empty, no accesses should be created.
"""
user = factories.UserFactory()
assert models.DocumentAccess.objects.filter(user=user).count() == 0
def test_models_users_handle_onboarding_document_link_trace_with_single_document():
"""
When USER_ONBOARDING_DOCUMENTS has a valid document ID,
a LinkTrace should be created for the new user.
The document should be pinned as a favorite for the user.
"""
document = factories.DocumentFactory(link_reach=models.LinkReachChoices.PUBLIC)
with override_settings(USER_ONBOARDING_DOCUMENTS=[str(document.id)]):
user = factories.UserFactory()
assert models.LinkTrace.objects.filter(user=user, document=document).count() == 1
user_favorites = models.DocumentFavorite.objects.filter(user=user)
assert user_favorites.count() == 1
assert user_favorites.filter(document=document).exists()
def test_models_users_handle_onboarding_documents_access_with_multiple_documents():
"""
When USER_ONBOARDING_DOCUMENTS has multiple valid document IDs,
accesses should be created for all documents.
All accesses should have the READER role.
All documents should be pinned as favorites for the user.
"""
document1 = factories.DocumentFactory(
title="Document 1", link_reach=models.LinkReachChoices.PUBLIC
)
document2 = factories.DocumentFactory(
title="Document 2", link_reach=models.LinkReachChoices.AUTHENTICATED
)
document3 = factories.DocumentFactory(
title="Document 3", link_reach=models.LinkReachChoices.PUBLIC
)
with override_settings(
USER_ONBOARDING_DOCUMENTS=[
str(document1.id),
str(document2.id),
str(document3.id),
]
):
user = factories.UserFactory()
link_traces = models.LinkTrace.objects.filter(user=user)
assert link_traces.count() == 3
assert models.LinkTrace.objects.filter(user=user, document=document1).exists()
assert models.LinkTrace.objects.filter(user=user, document=document2).exists()
assert models.LinkTrace.objects.filter(user=user, document=document3).exists()
user_favorites = models.DocumentFavorite.objects.filter(user=user)
assert user_favorites.count() == 3
assert user_favorites.filter(document=document1).exists()
assert user_favorites.filter(document=document2).exists()
assert user_favorites.filter(document=document3).exists()
def test_models_users_handle_onboarding_documents_access_with_invalid_document_id():
"""
When USER_ONBOARDING_DOCUMENTS has an invalid document ID,
it should be skipped and logged, but not raise an exception.
"""
invalid_id = uuid.uuid4()
with override_settings(USER_ONBOARDING_DOCUMENTS=[str(invalid_id)]):
with patch("core.models.logger") as mock_logger:
user = factories.UserFactory()
mock_logger.warning.assert_called_once()
call_args = mock_logger.warning.call_args
assert "Onboarding document with id" in call_args[0][0]
assert models.LinkTrace.objects.filter(user=user).count() == 0
def test_models_users_handle_onboarding_documents_access_duplicate_prevention():
"""
If the same document is listed multiple times in USER_ONBOARDING_DOCUMENTS,
it should only create one access (or handle duplicates gracefully).
"""
document = factories.DocumentFactory(link_reach=models.LinkReachChoices.PUBLIC)
with override_settings(
USER_ONBOARDING_DOCUMENTS=[str(document.id), str(document.id)]
):
user = factories.UserFactory()
link_traces = models.LinkTrace.objects.filter(user=user, document=document)
assert link_traces.count() == 1
def test_models_users_handle_onboarding_documents_on_restricted_document_is_not_allowed():
"""On-boarding document can be used when restricted"""
document = factories.DocumentFactory(link_reach=models.LinkReachChoices.RESTRICTED)
with override_settings(USER_ONBOARDING_DOCUMENTS=[str(document.id)]):
user = factories.UserFactory()
assert not models.LinkTrace.objects.filter(user=user, document=document).exists()
@override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=None)
def test_models_users_duplicate_onboarding_sandbox_document_no_setting():
"""
When USER_ONBOARDING_SANDBOX_DOCUMENT is not set, no sandbox document should be created.
"""
user = factories.UserFactory()
assert (
models.Document.objects.filter(creator=user, title__icontains="Sandbox").count()
== 0
)
initial_accesses = models.DocumentAccess.objects.filter(user=user).count()
assert initial_accesses == 0
def test_models_users_duplicate_onboarding_sandbox_document_creates_sandbox():
"""
When USER_ONBOARDING_SANDBOX_DOCUMENT is set with a valid template document,
a new sandbox document should be created for the user with OWNER access.
"""
template_document = factories.DocumentFactory(title="Getting started with Docs")
with override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=str(template_document.id)):
user = factories.UserFactory()
sandbox_docs = models.Document.objects.filter(
creator=user, title="Getting started with Docs"
)
assert sandbox_docs.count() == 1
sandbox_doc = sandbox_docs.first()
assert sandbox_doc.creator == user
assert sandbox_doc.duplicated_from == template_document
access = models.DocumentAccess.objects.get(user=user, document=sandbox_doc)
assert access.role == models.RoleChoices.OWNER
def test_models_users_duplicate_onboarding_sandbox_document_with_invalid_template_id():
"""
When USER_ONBOARDING_SANDBOX_DOCUMENT has an invalid document ID,
it should be skipped and logged, but not raise an exception.
"""
invalid_id = uuid.uuid4()
with override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=str(invalid_id)):
with patch("core.models.logger") as mock_logger:
user = factories.UserFactory()
mock_logger.warning.assert_called_once()
call_args = mock_logger.warning.call_args
assert "Onboarding sandbox document with id" in call_args[0][0]
sandbox_docs = models.Document.objects.filter(creator=user)
assert sandbox_docs.count() == 0
def test_models_users_duplicate_onboarding_sandbox_document_creates_unique_sandbox_per_user():
"""
Each new user should get their own independent sandbox document.
"""
template_document = factories.DocumentFactory(title="Getting started with Docs")
with override_settings(USER_ONBOARDING_SANDBOX_DOCUMENT=str(template_document.id)):
user1 = factories.UserFactory()
user2 = factories.UserFactory()
sandbox_docs_user1 = models.Document.objects.filter(
creator=user1, title="Getting started with Docs"
)
sandbox_docs_user2 = models.Document.objects.filter(
creator=user2, title="Getting started with Docs"
)
assert sandbox_docs_user1.count() == 1
assert sandbox_docs_user2.count() == 1
assert sandbox_docs_user1.first().id != sandbox_docs_user2.first().id
def test_models_users_duplicate_onboarding_sandbox_document_integration_with_other_methods():
"""
Verify that sandbox creation works alongside other onboarding methods.
"""
template_document = factories.DocumentFactory(title="Getting started with Docs")
onboarding_doc = factories.DocumentFactory(
title="Onboarding Document", link_reach=models.LinkReachChoices.AUTHENTICATED
)
with override_settings(
USER_ONBOARDING_SANDBOX_DOCUMENT=str(template_document.id),
USER_ONBOARDING_DOCUMENTS=[str(onboarding_doc.id)],
):
user = factories.UserFactory()
sandbox_doc = models.Document.objects.filter(
creator=user, title="Getting started with Docs"
).first()
assert models.DocumentAccess.objects.filter(user=user).count() == 1
assert models.LinkTrace.objects.filter(user=user).count() == 1
assert models.DocumentAccess.objects.filter(
document=sandbox_doc, user=user, role=models.RoleChoices.OWNER
).exists()
assert models.LinkTrace.objects.filter(document=onboarding_doc, user=user).exists()
@pytest.mark.django_db(transaction=True)
def test_models_users_duplicate_onboarding_sandbox_race_condition():
"""
It should be possible to create several documents at the same time
without causing any race conditions or data integrity issues.
"""
def create_user():
return factories.UserFactory()
template_document = factories.DocumentFactory(title="Getting started with Docs")
with (
override_settings(
USER_ONBOARDING_SANDBOX_DOCUMENT=str(template_document.id),
),
ThreadPoolExecutor(max_workers=2) as executor,
):
future1 = executor.submit(create_user)
future2 = executor.submit(create_user)
user1 = future1.result()
user2 = future2.result()
assert isinstance(user1, models.User)
assert isinstance(user2, models.User)

View File

@@ -1,7 +1,9 @@
"""
Test ai API endpoints in the impress core app.
Test AI services in the impress core app.
"""
# pylint: disable=protected-access
from collections.abc import AsyncIterator
from unittest.mock import MagicMock, patch
from django.core.exceptions import ImproperlyConfigured
@@ -9,12 +11,33 @@ from django.test.utils import override_settings
import pytest
from openai import OpenAIError
from pydantic_ai.ui.vercel_ai.request_types import TextUIPart, UIMessage
from core.services.ai_services import AIService
from core.services.ai_services import (
BLOCKNOTE_TOOL_STRICT_PROMPT,
AIService,
convert_async_generator_to_sync,
)
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.AI_BASE_URL = "http://example.com"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
settings.AI_FEATURE_BLOCKNOTE_ENABLED = True
settings.AI_FEATURE_LEGACY_ENABLED = True
settings.LANGFUSE_PUBLIC_KEY = None
settings.AI_VERCEL_SDK_VERSION = 6
# -- AIService.__init__ --
@pytest.mark.parametrize(
"setting_name, setting_value",
[
@@ -23,22 +46,25 @@ pytestmark = pytest.mark.django_db
("AI_MODEL", None),
],
)
def test_api_ai_setting_missing(setting_name, setting_value):
def test_services_ai_setting_missing(setting_name, setting_value, settings):
"""Setting should be set"""
setattr(settings, setting_name, setting_value)
with override_settings(**{setting_name: setting_value}):
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
AIService()
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
AIService()
# -- AIService.transform --
@override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="test-model"
)
@patch("openai.resources.chat.completions.Completions.create")
def test_api_ai__client_error(mock_create):
def test_services_ai_client_error(mock_create):
"""Fail when the client raises an error"""
mock_create.side_effect = OpenAIError("Mocked client error")
@@ -54,7 +80,7 @@ def test_api_ai__client_error(mock_create):
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="test-model"
)
@patch("openai.resources.chat.completions.Completions.create")
def test_api_ai__client_invalid_response(mock_create):
def test_services_ai_client_invalid_response(mock_create):
"""Fail when the client response is invalid"""
mock_create.return_value = MagicMock(
@@ -72,7 +98,7 @@ def test_api_ai__client_invalid_response(mock_create):
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="test-model"
)
@patch("openai.resources.chat.completions.Completions.create")
def test_api_ai__success(mock_create):
def test_services_ai_success(mock_create):
"""The AI request should work as expect when called with valid arguments."""
mock_create.return_value = MagicMock(
@@ -82,3 +108,483 @@ def test_api_ai__success(mock_create):
response = AIService().transform("hello", "prompt")
assert response == {"answer": "Salut"}
# -- AIService.translate --
@patch("openai.resources.chat.completions.Completions.create")
def test_services_ai_translate_success(mock_create):
"""Translate should call the AI API with the correct language prompt."""
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Bonjour"))]
)
response = AIService().translate("<p>Hello</p>", "fr")
assert response == {"answer": "Bonjour"}
call_args = mock_create.call_args
system_content = call_args[1]["messages"][0]["content"]
assert "French" in system_content or "fr" in system_content
@patch("openai.resources.chat.completions.Completions.create")
def test_services_ai_translate_unknown_language(mock_create):
"""Translate with an unknown language code should use the code as-is."""
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Translated"))]
)
response = AIService().translate("<p>Hello</p>", "xx-unknown")
assert response == {"answer": "Translated"}
call_args = mock_create.call_args
system_content = call_args[1]["messages"][0]["content"]
assert "xx-unknown" in system_content
# -- convert_async_generator_to_sync --
def test_convert_async_generator_to_sync_basic():
"""Should convert an async generator yielding items to a sync iterator."""
async def async_gen():
for item in ["hello", "world", "!"]:
yield item
result = list(convert_async_generator_to_sync(async_gen()))
assert result == ["hello", "world", "!"]
def test_convert_async_generator_to_sync_empty():
"""Should handle an empty async generator."""
async def async_gen():
return
yield
result = list(convert_async_generator_to_sync(async_gen()))
assert not result
def test_convert_async_generator_to_sync_exception():
"""Should propagate exceptions from the async generator."""
async def async_gen():
yield "first"
raise ValueError("async error")
sync_iter = convert_async_generator_to_sync(async_gen())
assert next(sync_iter) == "first"
with pytest.raises(ValueError, match="async error"):
next(sync_iter)
# -- AIService.inject_document_state_messages --
def test_inject_document_state_messages_no_metadata():
"""Messages without documentState metadata should pass through unchanged."""
messages = [
UIMessage(role="user", id="msg-1", parts=[TextUIPart(text="Hello")]),
]
result = AIService.inject_document_state_messages(messages)
assert len(result) == 1
assert result[0].id == "msg-1"
def test_inject_document_state_messages_with_selection():
"""A user message with documentState and selection should get an
assistant context message prepended."""
messages = [
UIMessage(
role="user",
id="msg-1",
parts=[TextUIPart(text="Fix this")],
metadata={
"documentState": {
"selection": {"start": 0, "end": 5},
"selectedBlocks": [{"type": "paragraph", "content": "Hello"}],
"blocks": [
{"type": "paragraph", "content": "Hello"},
{"type": "paragraph", "content": "World"},
],
}
},
),
]
result = AIService.inject_document_state_messages(messages)
assert len(result) == 2
# First message should be the injected assistant context
assert result[0].role == "assistant"
assert result[0].id == "assistant-document-state-msg-1"
assert len(result[0].parts) == 4
assert "selection" in result[0].parts[0].text.lower()
# Second message should be the original user message
assert result[1].id == "msg-1"
def test_inject_document_state_messages_without_selection():
"""A user message with documentState but no selection should describe
the full document context."""
messages = [
UIMessage(
role="user",
id="msg-1",
parts=[TextUIPart(text="Summarize")],
metadata={
"documentState": {
"selection": None,
"blocks": [
{"type": "paragraph", "content": "Hello"},
],
"isEmptyDocument": False,
}
},
),
]
result = AIService.inject_document_state_messages(messages)
assert len(result) == 2
assistant_msg = result[0]
assert assistant_msg.role == "assistant"
assert len(assistant_msg.parts) == 2
assert "no active selection" in assistant_msg.parts[0].text.lower()
assert "prefer updating" in assistant_msg.parts[0].text.lower()
def test_inject_document_state_messages_empty_document():
"""When the document is empty, the injected message should instruct
updating the empty block first."""
messages = [
UIMessage(
role="user",
id="msg-1",
parts=[TextUIPart(text="Write something")],
metadata={
"documentState": {
"selection": None,
"blocks": [{"type": "paragraph", "content": ""}],
"isEmptyDocument": True,
}
},
),
]
result = AIService.inject_document_state_messages(messages)
assert len(result) == 2
assistant_msg = result[0]
assert "update the empty block" in assistant_msg.parts[0].text.lower()
def test_inject_document_state_messages_mixed():
"""Only user messages with documentState get assistant context;
other messages pass through unchanged."""
messages = [
UIMessage(
role="assistant",
id="msg-0",
parts=[TextUIPart(text="Previous response")],
),
UIMessage(
role="user",
id="msg-1",
parts=[TextUIPart(text="Hello")],
),
UIMessage(
role="user",
id="msg-2",
parts=[TextUIPart(text="Fix this")],
metadata={
"documentState": {
"selection": {"start": 0, "end": 5},
"selectedBlocks": [{"type": "paragraph", "content": "Hello"}],
"blocks": [{"type": "paragraph", "content": "Hello"}],
}
},
),
]
result = AIService.inject_document_state_messages(messages)
# 3 original + 1 injected assistant message before msg-2
assert len(result) == 4
assert result[0].id == "msg-0"
assert result[1].id == "msg-1"
assert result[2].role == "assistant"
assert result[2].id == "assistant-document-state-msg-2"
assert result[3].id == "msg-2"
# -- AIService.tool_definitions_to_toolset --
def test_tool_definitions_to_toolset():
"""Should convert frontend tool definitions to an ExternalToolset."""
tool_definitions = {
"applyOperations": {
"description": "Apply operations to the document",
"inputSchema": {
"type": "object",
"properties": {
"operations": {"type": "array"},
},
},
"outputSchema": {"type": "object"},
},
"insertBlocks": {
"description": "Insert blocks",
"inputSchema": {"type": "object"},
},
}
toolset = AIService.tool_definitions_to_toolset(tool_definitions)
# The ExternalToolset wraps ToolDefinition objects
assert toolset is not None
# Access internal tool definitions
tool_defs = toolset.tool_defs
assert len(tool_defs) == 2
names = {td.name for td in tool_defs}
assert names == {"applyOperations", "insertBlocks"}
for td in tool_defs:
assert td.kind == "external"
if td.name == "applyOperations":
assert td.description == "Apply operations to the document"
assert td.metadata == {"output_schema": {"type": "object"}}
def test_tool_definitions_to_toolset_missing_fields():
"""Should handle tool definitions with missing optional fields."""
tool_definitions = {
"myTool": {},
}
toolset = AIService.tool_definitions_to_toolset(tool_definitions)
tool_defs = toolset.tool_defs
assert len(tool_defs) == 1
assert tool_defs[0].name == "myTool"
assert tool_defs[0].description == ""
assert tool_defs[0].parameters_json_schema == {}
assert tool_defs[0].metadata == {"output_schema": None}
# -- AIService.stream --
@patch.object(AIService, "_build_async_stream")
def test_services_ai_stream_sync_mode(mock_build, monkeypatch):
"""In sync mode, stream() should return a sync iterator."""
async def mock_async_gen():
yield "chunk1"
yield "chunk2"
mock_build.return_value = mock_async_gen()
monkeypatch.setenv("PYTHON_SERVER_MODE", "sync")
service = AIService()
request = MagicMock()
result = service.stream(request)
# Should be a regular (sync) iterator, not async
assert not isinstance(result, AsyncIterator)
assert list(result) == ["chunk1", "chunk2"]
mock_build.assert_called_once_with(request)
@patch.object(AIService, "_build_async_stream")
def test_services_ai_stream_async_mode(mock_build, monkeypatch):
"""In async mode, stream() should return the async iterator directly."""
async def mock_async_gen():
yield "chunk1"
yield "chunk2"
mock_async_iter = mock_async_gen()
mock_build.return_value = mock_async_iter
monkeypatch.setenv("PYTHON_SERVER_MODE", "async")
service = AIService()
request = MagicMock()
result = service.stream(request)
assert result is mock_async_iter
mock_build.assert_called_once_with(request)
@patch.object(AIService, "_build_async_stream")
def test_services_ai_stream_defaults_to_sync(mock_build, monkeypatch):
"""When PYTHON_SERVER_MODE is not set, stream() should default to sync."""
async def mock_async_gen():
yield "data"
mock_build.return_value = mock_async_gen()
monkeypatch.delenv("PYTHON_SERVER_MODE", raising=False)
service = AIService()
request = MagicMock()
result = service.stream(request)
# Default should be sync mode
assert not isinstance(result, AsyncIterator)
assert list(result) == ["data"]
# -- AIService._build_async_stream --
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream(mock_adapter_cls):
"""_build_async_stream should build the pydantic-ai streaming pipeline."""
async def mock_encode():
yield "event-data"
mock_run_input = MagicMock()
mock_run_input.model_extra = None
mock_run_input.messages = []
mock_adapter_cls.build_run_input.return_value = mock_run_input
mock_adapter_instance = MagicMock()
mock_adapter_instance.run_stream.return_value = MagicMock()
mock_adapter_instance.encode_stream.return_value = mock_encode()
mock_adapter_cls.return_value = mock_adapter_instance
service = AIService()
request = MagicMock()
request.META = {"HTTP_ACCEPT": "text/event-stream"}
request.raw_body = b'{"messages": []}'
result = service._build_async_stream(request)
assert isinstance(result, AsyncIterator)
mock_adapter_cls.build_run_input.assert_called_once_with(b'{"messages": []}')
mock_adapter_instance.run_stream.assert_called_once()
mock_adapter_instance.encode_stream.assert_called_once()
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_with_tool_definitions(mock_adapter_cls):
"""_build_async_stream should build an ExternalToolset when
toolDefinitions are present in the request."""
async def mock_encode():
yield "event-data"
mock_run_input = MagicMock()
mock_run_input.model_extra = {
"toolDefinitions": {
"myTool": {
"description": "A tool",
"inputSchema": {"type": "object"},
}
}
}
mock_run_input.messages = []
mock_adapter_cls.build_run_input.return_value = mock_run_input
mock_adapter_instance = MagicMock()
mock_adapter_instance.run_stream.return_value = MagicMock()
mock_adapter_instance.encode_stream.return_value = mock_encode()
mock_adapter_cls.return_value = mock_adapter_instance
service = AIService()
request = MagicMock()
request.META = {}
request.raw_body = b"{}"
service._build_async_stream(request)
# run_stream should have been called with a toolset
call_kwargs = mock_adapter_instance.run_stream.call_args[1]
assert call_kwargs["toolsets"] is not None
assert len(call_kwargs["toolsets"]) == 1
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_with_tool_definitions_required_system_prompt(
mock_adapter_cls,
):
"""The presence of the applyDocumentOperations tool must force the addition
of a system prompt"""
async def mock_encode():
yield "event-data"
mock_run_input = MagicMock()
mock_run_input.model_extra = {
"toolDefinitions": {
"applyDocumentOperations": {
"description": "A tool",
"inputSchema": {"type": "object"},
}
}
}
mock_run_input.messages = []
mock_adapter_cls.build_run_input.return_value = mock_run_input
mock_adapter_instance = MagicMock()
mock_adapter_instance.run_stream.return_value = MagicMock()
mock_adapter_instance.encode_stream.return_value = mock_encode()
mock_adapter_cls.return_value = mock_adapter_instance
service = AIService()
request = MagicMock()
request.META = {}
request.raw_body = b"{}"
service._build_async_stream(request)
# run_stream should have been called with a toolset
call_kwargs = mock_adapter_instance.run_stream.call_args[1]
assert call_kwargs["toolsets"] is not None
assert len(call_kwargs["toolsets"]) == 1
assert len(mock_run_input.messages) == 1
assert mock_run_input.messages[0].id == "system-force-tool-usage"
assert mock_run_input.messages[0].role == "system"
assert mock_run_input.messages[0].parts[0].text == BLOCKNOTE_TOOL_STRICT_PROMPT
@patch("core.services.ai_services.Agent")
@patch("core.services.ai_services.VercelAIAdapter")
def test_services_ai_build_async_stream_langfuse_enabled(
mock_adapter_cls, mock_agent_cls, settings
):
"""When LANGFUSE_PUBLIC_KEY is set, instrument should be enabled."""
settings.LANGFUSE_PUBLIC_KEY = "pk-test-123"
async def mock_encode():
yield "data"
mock_run_input = MagicMock()
mock_run_input.model_extra = None
mock_run_input.messages = []
mock_adapter_cls.build_run_input.return_value = mock_run_input
mock_adapter_instance = MagicMock()
mock_adapter_instance.run_stream.return_value = MagicMock()
mock_adapter_instance.encode_stream.return_value = mock_encode()
mock_adapter_cls.return_value = mock_adapter_instance
service = AIService()
request = MagicMock()
request.META = {}
request.raw_body = b"{}"
service._build_async_stream(request)
mock_agent_cls.instrument_all.assert_called_once()
# Agent should be created with instrument=True
mock_agent_cls.assert_called_once()
assert mock_agent_cls.call_args[1]["instrument"] is True

View File

@@ -138,6 +138,7 @@ def create_demo(stdout):
password="!",
is_superuser=False,
is_active=True,
is_first_connection=False,
is_staff=False,
short_name=first_name,
full_name=f"{first_name:s} {random.choice(last_names):s}",
@@ -194,6 +195,7 @@ def create_demo(stdout):
password="!",
is_superuser=False,
is_active=True,
is_first_connection=False,
is_staff=False,
language=dev_user["language"] or random.choice(languages),
)

View File

@@ -0,0 +1,18 @@
"""
ASGI config for impress project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/dev/howto/deployment/asgi/
"""
import os
from configurations.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "impress.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
os.environ.setdefault("PYTHON_SERVER_MODE", "async")
application = get_asgi_application()

View File

@@ -158,5 +158,9 @@
"href": "/assets/favicon-dark.png",
"type": "image/png"
}
},
"onboarding": {
"enabled": true,
"learn_more_url": ""
}
}

View File

@@ -99,6 +99,24 @@ class Base(Configuration):
"localhost", environ_name="DB_HOST", environ_prefix=None
),
"PORT": values.Value(5432, environ_name="DB_PORT", environ_prefix=None),
"OPTIONS": {
# https://www.psycopg.org/psycopg3/docs/api/pool.html#psycopg_pool.ConnectionPool
"pool": {
"min_size": values.IntegerValue(
4, environ_name="DB_PSYCOPG_POOL_MIN_SIZE", environ_prefix=None
),
"max_size": values.IntegerValue(
None,
environ_name="DB_PSYCOPG_POOL_MAX_SIZE",
environ_prefix=None,
),
"timeout": values.IntegerValue(
3,
environ_name="DB_PSYCOPG_POOL_TIMEOUT",
environ_prefix=None,
),
}
},
}
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
@@ -326,6 +344,7 @@ class Base(Configuration):
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"core.middleware.ForceSessionMiddleware",
"core.middleware.SaveRawBodyMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"dockerflow.django.middleware.DockerflowMiddleware",
"csp.middleware.CSPMiddleware",
@@ -687,24 +706,48 @@ class Base(Configuration):
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
)
# AI service
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
# AI settings
AI_ALLOW_REACH_FROM = values.Value(
choices=("public", "authenticated", "restricted"),
default="authenticated",
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_BOT = values.DictValue(
default={
"name": _("Docs AI"),
"color": "#8bc6ff",
},
environ_name="AI_BOT",
environ_prefix=None,
)
AI_DOCUMENT_RATE_THROTTLE_RATES = {
"minute": 5,
"hour": 100,
"day": 500,
}
# Master settings to enable AI features, if you set it to False,
# all AI features will be disabled even if the other settings are enabled.
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
# Far better UI but more flaky for the moment
# ⚠️ AGPL license, be sure to comply with the Blocknote license
# if you enable it (https://www.blocknotejs.org/)
AI_FEATURE_BLOCKNOTE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_BLOCKNOTE_ENABLED", environ_prefix=None
)
# UI with less features but more stable
# MIT friendly license, you can enable it without worrying about the license
AI_FEATURE_LEGACY_ENABLED = values.BooleanValue(
default=True, environ_name="AI_FEATURE_LEGACY_ENABLED", environ_prefix=None
)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_VERCEL_SDK_VERSION = values.IntegerValue(
6, environ_name="AI_VERCEL_SDK_VERSION", environ_prefix=None
)
AI_USER_RATE_THROTTLE_RATES = {
"minute": 3,
"hour": 50,
@@ -736,7 +779,7 @@ class Base(Configuration):
# Imported file settings
CONVERSION_FILE_MAX_SIZE = values.IntegerValue(
20 * MB, # 10MB
20 * MB,
environ_name="CONVERSION_FILE_MAX_SIZE",
environ_prefix=None,
)
@@ -884,7 +927,12 @@ class Base(Configuration):
USER_RECONCILIATION_FORM_URL = values.Value(
None, environ_name="USER_RECONCILIATION_FORM_URL", environ_prefix=None
)
USER_ONBOARDING_DOCUMENTS = values.ListValue(
[], environ_name="USER_ONBOARDING_DOCUMENTS", environ_prefix=None
)
USER_ONBOARDING_SANDBOX_DOCUMENT = values.Value(
None, environ_name="USER_ONBOARDING_SANDBOX_DOCUMENT", environ_prefix=None
)
# Marketing and communication settings
SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue(
False,

View File

@@ -13,5 +13,6 @@ from configurations.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "impress.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
os.environ.setdefault("PYTHON_SERVER_MODE", "sync")
application = get_wsgi_application()

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Titouroù personel"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Aotreoù"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Deiziadoù a-bouez"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Gwezennadur"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Titl"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Me eo an aozer"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr "Kuzhet"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -135,262 +143,382 @@ msgstr "Kleiz"
msgid "Right"
msgstr "Dehoù"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "alc'hwez kentañ evit an enrollañ evel UIID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "krouet d'ar/al"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "deiziad hag eurvezh krouidigezh an enrolladenn"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "hizivaet d'ar/al"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "deiziad hag eurvezh m'eo bet hizivaet an enrolladenn"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "N'hon eus kavet implijer ebet gant an isstrollad-mañ met ar postel a zo liammet ouzh un implijer enrollet."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "isstrollad"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "anv klok"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "anv berr"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "postel identelezh"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "postel ar merour"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "yezh"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "trevnad"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "statud ar skipailh"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "oberiant"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "implijer"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "titl"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "bomm"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Restr"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Digeriñ"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Persönliche Daten"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Berechtigungen"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Wichtige Daten"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Baumstruktur"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Ersteller bin ich"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -135,262 +143,382 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "primärer Schlüssel für den Datensatz als UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "Erstellt"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "Datum und Uhrzeit, an dem ein Datensatz erstellt wurde"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "Aktualisiert"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wir konnten keinen Benutzer mit diesem Abo finden, aber die E-Mail-Adresse ist bereits einem registrierten Benutzer zugeordnet."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "unter"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "Auszug"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Öffnen"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr ""
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -135,262 +143,382 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr ""
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr ""
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr ""
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr ""
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Información Personal"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permisos"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Fechas importantes"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Estructura en árbol"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Título"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Yo soy el creador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -135,262 +143,382 @@ msgstr "Izquierda"
msgid "Right"
msgstr "Derecha"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "clave primaria para el registro como UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "creado el"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "fecha y hora en la que se creó un registro"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "actualizado el"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "fecha y hora en la que un registro fue actualizado por última vez"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "No se ha podido encontrar un usuario con este sub (UUID), pero el correo electrónico ya está asociado con un usuario."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "sub (UUID)"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "nombre completo"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "nombre abreviado"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "correo electrónico de identidad"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "correo electrónico del administrador"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "idioma"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "El idioma en el que el usuario desea ver la interfaz."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Si el usuario es un dispositivo o un usuario real."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "rol en el equipo"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Si el usuario puede iniciar sesión en esta página web de administración."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "activo"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si este usuario debe ser considerado como activo. Deseleccionar en lugar de eliminar cuentas."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "usuario"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "título"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "resumen"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Abrir"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Ya existe una traza de enlace para este documento/usuario."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Este documento ya ha sido marcado como favorito por el usuario."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "Debe establecerse un usuario o un equipo, no ambos."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr "Este usuario ya ha solicitado acceso a este documento."
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Infos Personnelles"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permissions"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Tâche d'importation créée et mise en file d'attente."
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Traiter les rapprochements de l'utilisateur sélectionné"
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Arborescence"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Titre"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Je suis l'auteur"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr "Masqué"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -135,262 +143,389 @@ msgstr "Gauche"
msgid "Right"
msgstr "Droite"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "identifiant/id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "clé primaire pour l'enregistrement en tant que UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "créé le"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "date et heure de création de l'enregistrement"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "mis à jour le"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Nous n'avons pas pu trouver un utilisateur avec ce sous-groupe mais l'e-mail est déjà associé à un utilisateur enregistré."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "sous-groupe"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Obligatoire. 255 caractères ou moins. Caractères ASCII uniquement."
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "nom complet"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "nom court"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "adresse e-mail d'identité"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "adresse e-mail de l'administrateur"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "langue"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "appareil"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "statut d'équipe"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "actif"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr "état de la première connexion"
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr "Si l'utilisateur a terminé le processus de première connexion."
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "utilisateur"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr "Adresse email active"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr "Adresse email à désactiver"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr "Identifiant unique dans le fichier source"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr "En attente"
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr "Prêt"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr "Terminé"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr "Erreur"
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr "rapprochement de l'utilisateur"
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr "rapprochements de l'utilisateur"
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr "Vous avez demandé un rapprochement de vos comptes utilisateur sur Docs.\n"
" Pour confirmer que vous êtes bien à l'origine de cette demande\n"
" et que cet e-mail vous appartient :"
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Confirmez en cliquant sur le lien pour commencer le rapprochement"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr "Cliquez ici"
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr "Confirmer"
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Votre demande de rapprochement a été traitée.\n"
" De nouveaux documents sont probablement associés à votre compte :"
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr "Vos comptes ont été fusionnés"
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr "Cliquez ici pour voir"
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr "Voir mes documents"
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr "Fichier CSV"
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr "En cours"
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr "importation CSV de rapprochement utilisateur"
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr "importations CSV de rapprochement utilisateur"
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr "Votre demande de rapprochement n'a pas abouti.\n"
" Le rapprochement a échoué pour les adresses e-mail suivantes :\n"
" {recipient_email}, {other_email}.\n"
" Veuillez vérifier qu'il n'y a pas de fautes de frappe.\n"
" Vous pouvez envoyer une nouvelle demande avec des adresses e-mail valides."
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Le rapprochement de vos comptes Docs n'est pas terminé"
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr "Faire une nouvelle demande"
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "titre"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "extrait"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Ouvrir"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant :"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} a partagé un document avec vous : {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ce document est déjà un favori de cet utilisateur."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} souhaiterait accéder au document suivant :"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} demande l'accès au document : {title}"
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr "Conversation"
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr "Conversations"
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr "Anonyme"
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr "Commentaire"
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr "Commentaires"
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr "Cet émoji a déjà été réagi à ce commentaire."
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr "Réaction"
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr "Réactions"
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr "Docs IA"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Informazioni personali"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permessi"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Date importanti"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Struttura ad albero"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Titolo"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Il creatore sono io"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -135,262 +143,382 @@ msgstr "Sinistra"
msgid "Right"
msgstr "Destra"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "Id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "chiave primaria per il record come UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "creato il"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "data e ora in cui è stato creato un record"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "aggiornato il"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "data e ora in cui lultimo record è stato aggiornato"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "nome completo"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "nome"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "indirizzo email di identità"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "Indirizzo email dell'amministratore"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "lingua"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Se l'utente è un dispositivo o un utente reale."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "stato del personale"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Indica se l'utente può accedere a questo sito amministratore."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "attivo"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Indica se questo utente deve essere trattato come attivo. Deseleziona invece di eliminare gli account."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "utente"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "titolo"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Apri"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha condiviso un documento con te: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Persoonlijke informatie"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Machtigingen"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Belangrijke data"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Import taak gemaakt en in de wachtrij geplaatst."
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Verwerk geselecteerde gebruikers samenvoeging"
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Boomstructuur"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Ik ben eigenaar"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr "Gemaskeerd"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Favoriet"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document is namens u gemaakt!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -135,262 +143,389 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "primaire sleutel voor dossier als UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "gecreëerd op"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "datum en tijd waarop dossier is gecreeërd"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "Laatst gewijzigd op"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "datum en tijd waarop dossier laatst was gewijzigd"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wij konden geen gebruiker vinden met dit id, maar de email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "id"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Vereist. 255 tekens of minder. Alleen ASCII tekens."
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "volledige naam"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "gebruikersnaam"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "identiteit emailadres"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "admin emailadres"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "taal"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wil zien."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "apparaat"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "beheerder status"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen in het beheer gedeelte."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "actief"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "gebruiker"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr "Actieve e-mail adres"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr "E-mailadres om te deactiveren"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr "Unieke ID in het bronbestand"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr "In behandeling"
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr "Klaar"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr "Klaar"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr "Fout"
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr "gebruiker samenvoegen"
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr "gebruikers samenvoegen"
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr "Je hebt gevraagd om een samenvoeging van je gebruikersaccounts op Docs.\n"
" Om te bevestigen dat u degene bent die het verzoek\n"
" heeft geïnitieerd en dat deze e-mail van u is:"
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Bevestig door te klikken op de link om de samenvoeging te starten"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr "Klik hier"
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr "Bevestig"
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Uw samenvoegingsverzoek is verwerkt.\n"
" Nieuwe documenten worden waarschijnlijk geassocieerd met uw account:"
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr "Je accounts zijn samengevoegd"
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr "Klik hier om te bekijken"
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr "Mijn documenten bekijken"
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr "CSV bestand"
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr "Bezig"
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr "gebruiker samenvoeging CSV import"
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr "gebruiker reconciliation CSV imports"
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr "Uw verzoek tot verzoening is mislukt.\n"
" Verzoening mislukt voor de volgende e-mailadressen:\n"
" {recipient_email}, {other_email}.\n"
" Controleer op typefouten.\n"
" U kunt een ander verzoek indienen met de geldige e-mailadressen."
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Samenvoeging van je Docs accounts is niet voltooid"
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr "Maak een nieuw verzoek"
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "titel"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "uittreksel"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Open"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met u gedeeld!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} heeft een document met u gedeeld: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Een link bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "De gebruiker bestaat al in dit document."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr "Document verzoekt om toegang"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr "Document verzoekt om toegangen"
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} verzoekt toegang tot het volgende document:"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} verzoekt toegang tot het document: {title}"
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr "Kanaal"
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr "Kanalen"
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr "Anoniem"
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr "Reactie"
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr "Reacties"
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr "Deze emoji is al op deze opmerking gereageerd."
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr "Reactie"
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr "Reacties"
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr "Docs AI"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Informações Pessoais"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Permissões"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Datas importantes"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Estrutura de árvore"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Título"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Eu sou o criador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "A propriedade de um novo documento foi concedida a você:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -135,262 +143,382 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr ""
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr ""
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr ""
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr ""
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Личная информация"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Разрешения"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Задание по импорту создано и поставлено в очередь."
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Обработка выбранных пользовательских сверок"
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Древовидная структура"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Создатель - я"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr "Скрытый"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -135,262 +143,389 @@ msgstr "Слева"
msgid "Right"
msgstr "Справа"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "первичный ключ для записи как UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "создано"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "дата и время создания записи"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "обновлено"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "дата и время последнего обновления записи"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Мы не смогли найти пользователя с этими данными, но этот адрес уже связан с зарегистрированным пользователем."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "вложение"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Обязательно. 255 символов или меньше. Только ASCII символы."
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "полное имя"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "короткое имя"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "личный адрес электронной почты"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "e-mail администратора"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "язык"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "устройство"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Пользователь является устройством или человеком."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "статус сотрудника"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Может ли пользователь войти на этот административный сайт."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "активный"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr "состояние первого подключения"
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr "Завершил ли пользователь процесс первого соединения."
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "пользователь"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr "Активный адрес электронной почты"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr "Адрес электронной почты для деактивации"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr "Уникальный идентификатор в исходном файле"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr "В обработке"
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr "Готово"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr "Выполнено"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr "Ошибка"
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr "сверка данных пользователя"
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr "сверки данных пользователя"
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr "Вы запросили сверку учётных записей пользователя в Docs.\n"
" Чтобы подтвердить факт того, что вы являетесь инициатором запроса\n"
" и что этот адрес принадлежит вам:"
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Чтобы начать сверку, подтвердите это, нажав на ссылку"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr "Нажмите здесь"
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr "Подтверждение"
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Ваш запрос на сверку был обработан.\n"
" Новые документы, вероятно, связаны с вашей учётной записью:"
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr "Ваши учётные записи были объединены"
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr "Нажмите здесь, чтобы просмотреть"
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr "Просмотреть мои документы"
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr "CSV-файл"
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr "Выполнение"
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr "импорт из CSV сверки пользователей"
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr "импорты из CSV сверки пользователями"
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr "Ваш запрос на сверку не удался.\n"
" Сверка не удалась для следующих email адресов:\n"
" {recipient_email}, {other_email}.\n"
" Пожалуйста, проверьте, нет ли в них опечаток.\n"
" Вы можете отправить ещё один запрос с действительными адресами электронной почты."
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Сверка ваших учётных записей Docs не завершена"
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr "Создать новый запрос"
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "отрывок"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Открыть"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr "Обсуждение"
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr "Обсуждения"
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr "Аноним"
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr "Комментарий"
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr "Комментарии"
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr "Этот эмодзи уже использован в этом комментарии."
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr "Реакция"
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr "Реакции"
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr "Docs ИИ"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Osebni podatki"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Dovoljenja"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Pomembni datumi"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Drevesna struktura"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Naslov"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Ustvaril sem jaz"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -135,262 +143,382 @@ msgstr "Levo"
msgid "Right"
msgstr "Desno"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "primarni ključ za zapis kot UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "ustvarjen na"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "datum in čas, ko je bil zapis ustvarjen"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "posodobljeno dne"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "datum in čas, ko je bil zapis nazadnje posodobljen"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Nismo mogli najti uporabnika s tem sub, vendar je e-poštni naslov že povezan z registriranim uporabnikom."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "polno ime"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "kratko ime"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "elektronski naslov identitete"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "elektronski naslov skrbnika"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "jezik"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "naprava"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "kadrovski status"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "aktivni"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ali je treba tega uporabnika obravnavati kot aktivnega. Namesto brisanja računov počistite to izbiro."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "uporabnik"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "naslov"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "odlomek"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Odpri"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ta dokument je že ciljno usmerjen s priljubljenim primerkom relacije za istega uporabnika."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Personuppgifter"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Behörigheter"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Viktiga datum"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Skaparen är jag"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -135,262 +143,382 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr ""
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr ""
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "aktiv"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr ""
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Öppna"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr ""
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -135,262 +143,382 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr ""
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr ""
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr ""
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr ""
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr ""
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Дозволи"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Завдання імпорту створено і поставлено в чергу."
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Обробити обрані узгодження користувача"
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Ієрархічна структура"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "Творець — я"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr "Приховано"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -135,262 +143,389 @@ msgstr "Ліворуч"
msgid "Right"
msgstr "Праворуч"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "первинний ключ для запису як UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "створено"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "дата і час, коли запис було створено"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "оновлено"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "дата і час, коли запис був востаннє оновлений"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Ми не змогли знайти користувача з цими даними, але адреса вже пов'язана з зареєстрованим користувачем."
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "вкладений документ"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Обов'язкове. 255 символів або менше. Тільки символи ASCII."
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "повне ім'я"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "коротке ім'я"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "адреса електронної пошти особи"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "електронна адреса адміністратора"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "мова"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "Мова, якою користувач хоче бачити інтерфейс."
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Часовий пояс, в якому користувач хоче бачити час."
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "пристрій"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Чи є користувач пристроєм чи реальним користувачем."
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "статус співробітника"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Чи може користувач увійти на цей сайт адміністратора."
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "активний"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr "стан першого з'єднання"
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr "Чи завершив користувач перший процес з'єднання."
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "користувач"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr "Активна електронна адреса"
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr "Електронна адреса, що буде деактивована"
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr "Унікальний ідентифікатор у вихідному файлі"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr "В очікуванні"
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr "Готово"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr "Виконано"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr "Помилка"
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr "узгодження користувачів"
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr "узгодження користувачів"
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr "Ви запросили узгодження своїх облікових записів користувачів у Docs.\n"
" Щоб підтвердити, що саме ви ініціювали запит\n"
" і що ця електронна адреса належить вам:"
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Підтвердіть, натиснувши на посилання, щоб почати узгодження"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr "Натисніть тут"
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr "Підтвердження"
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Ваш запит на узгодження оброблено.\n"
" Нові документи, ймовірно, пов'язані з вашим обліковим записом:"
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr "Ваші облікові записи були об'єднані"
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr "Натисніть тут, щоб переглянути"
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr "Переглянути мої документи"
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr "CSV-файл"
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr "Виконується"
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr "імпорт CSV для узгодження користувачів"
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr "імпорт CSV для узгодження користувачів"
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr "Ваш запит на узгодження не був виконаний.\n"
" Узгодження не вдалося для наступних адрес електронної пошти:\n"
" {recipient_email}, {other_email}.\n"
" Перевірте, чи немає помилок.\n"
" Ви можете надіслати інший запит із дійсними адресами електронної пошти."
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Узгодження ваших облікових записів не завершено"
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr "Зробити новий запит"
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "уривок"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "Відкрити"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr "Обговорення"
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr "Обговорення"
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr "Анонім"
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr "Коментар"
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr "Коментарі"
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr "Цим емодзі вже відреагували на цей коментар."
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr "Реакція"
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr "Реакції"
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr "Docs ШІ"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -17,57 +17,65 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:28 core/admin.py:28
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "個人資訊"
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "權限"
#: build/lib/core/admin.py:53 core/admin.py:53
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "重要日期"
#: build/lib/core/admin.py:131 core/admin.py:131
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr ""
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "樹狀結構"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "標題"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
msgstr "建立者是我"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr "已隱藏"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Favorite"
msgstr "我的最愛"
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
msgid "A new document was created on your behalf!"
msgstr "已代表您建立新文件!"
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
msgid "You have been granted ownership of a new document:"
msgstr "您已獲得新文件的所有權:"
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
msgid "This field is required."
msgstr "此欄位為必填。"
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "根據父文件設定,不允許連結範圍「%(link_reach)s」。"
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -135,262 +143,382 @@ msgstr "左"
msgid "Right"
msgstr "右"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "ID"
#: build/lib/core/models.py:82 core/models.py:82
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "記錄的主鍵UUID"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "建立於"
#: build/lib/core/models.py:89 core/models.py:89
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "記錄建立的日期與時間"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "更新於"
#: build/lib/core/models.py:95 core/models.py:95
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "記錄最後更新的日期與時間"
#: build/lib/core/models.py:131 core/models.py:131
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "我們找不到具有此 sub 的使用者,但此電子郵件地址已與已註冊使用者關聯。"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "sub"
#: build/lib/core/models.py:143 core/models.py:143
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "必填。255 個字元(含)以下。僅限 ASCII 字元。"
#: build/lib/core/models.py:151 core/models.py:151
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "全名"
#: build/lib/core/models.py:153 core/models.py:153
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "簡稱"
#: build/lib/core/models.py:156 core/models.py:156
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "身份驗證電子郵件地址"
#: build/lib/core/models.py:161 core/models.py:161
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "管理員電子郵件地址"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "語言"
#: build/lib/core/models.py:169 core/models.py:169
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "使用者希望介面顯示的語言。"
#: build/lib/core/models.py:177 core/models.py:177
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "使用者希望時間顯示的時區。"
#: build/lib/core/models.py:180 core/models.py:180
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "裝置"
#: build/lib/core/models.py:182 core/models.py:182
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "使用者是裝置還是真實使用者。"
#: build/lib/core/models.py:185 core/models.py:185
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "工作人員狀態"
#: build/lib/core/models.py:187 core/models.py:187
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "使用者是否可以登入此管理後台。"
#: build/lib/core/models.py:190 core/models.py:190
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "啟用"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "此使用者是否應被視為處於啟用狀態。請取消勾選此項而非刪除帳號。"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "使用者"
#: build/lib/core/models.py:206 core/models.py:206
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "使用者"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:378 core/models.py:378
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
msgid "title"
msgstr "標題"
#: build/lib/core/models.py:363 core/models.py:363
#: build/lib/core/models.py:880 core/models.py:880
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:929 core/models.py:929
msgid "Document"
msgstr "文件"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:930 core/models.py:930
msgid "Documents"
msgstr "文件"
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
msgid "Untitled Document"
msgstr "未命名文件"
#: build/lib/core/models.py:829 core/models.py:829
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Open"
msgstr "開啟"
#: build/lib/core/models.py:864 core/models.py:864
#: build/lib/core/models.py:1382 core/models.py:1382
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 與您分享了一份文件!"
#: build/lib/core/models.py:868 core/models.py:868
#: build/lib/core/models.py:1386 core/models.py:1386
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀請您以「{role}」角色參與以下文件:"
#: build/lib/core/models.py:874 core/models.py:874
#: build/lib/core/models.py:1392 core/models.py:1392
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 與您分享了一份文件:{title}"
#: build/lib/core/models.py:975 core/models.py:975
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link trace"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:976 core/models.py:976
#: build/lib/core/models.py:1494 core/models.py:1494
msgid "Document/user link traces"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:982 core/models.py:982
#: build/lib/core/models.py:1500 core/models.py:1500
msgid "A link trace already exists for this document/user."
msgstr "此文件/使用者已存在連結追蹤。"
#: build/lib/core/models.py:1005 core/models.py:1005
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorite"
msgstr "文件收藏"
#: build/lib/core/models.py:1006 core/models.py:1006
#: build/lib/core/models.py:1524 core/models.py:1524
msgid "Document favorites"
msgstr "文件收藏"
#: build/lib/core/models.py:1012 core/models.py:1012
#: build/lib/core/models.py:1530 core/models.py:1530
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "此使用者已將此文件加入收藏。"
#: build/lib/core/models.py:1034 core/models.py:1034
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relation"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1553 core/models.py:1553
msgid "Document/user relations"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1041 core/models.py:1041
#: build/lib/core/models.py:1559 core/models.py:1559
msgid "This user is already in this document."
msgstr "此使用者已在此文件中。"
#: build/lib/core/models.py:1047 core/models.py:1047
#: build/lib/core/models.py:1565 core/models.py:1565
msgid "This team is already in this document."
msgstr "此團隊已在此文件中。"
#: build/lib/core/models.py:1053 core/models.py:1053
#: build/lib/core/models.py:1571 core/models.py:1571
msgid "Either user or team must be set, not both."
msgstr "必須設定使用者或團隊其中之一,不能同時設定兩者。"
#: build/lib/core/models.py:1204 core/models.py:1204
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for access"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1205 core/models.py:1205
#: build/lib/core/models.py:1723 core/models.py:1723
msgid "Document ask for accesses"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1211 core/models.py:1211
#: build/lib/core/models.py:1729 core/models.py:1729
msgid "This user has already asked for access to this document."
msgstr "此使用者已要求過存取此文件的權限。"
#: build/lib/core/models.py:1268 core/models.py:1268
#: build/lib/core/models.py:1786 core/models.py:1786
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 想要存取文件!"
#: build/lib/core/models.py:1272 core/models.py:1272
#: build/lib/core/models.py:1790 core/models.py:1790
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 想要存取以下文件:"
#: build/lib/core/models.py:1278 core/models.py:1278
#: build/lib/core/models.py:1796 core/models.py:1796
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} 正要求存取文件:{title}"
#: build/lib/core/models.py:1320 core/models.py:1320
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr "對話串"
#: build/lib/core/models.py:1321 core/models.py:1321
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr "對話串"
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
msgid "Anonymous"
msgstr "匿名"
#: build/lib/core/models.py:1371 core/models.py:1371
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr "評論"
#: build/lib/core/models.py:1372 core/models.py:1372
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr "評論"
#: build/lib/core/models.py:1421 core/models.py:1421
#: build/lib/core/models.py:1939 core/models.py:1939
msgid "This emoji has already been reacted to this comment."
msgstr "此評論已標記過此表情符號。"
#: build/lib/core/models.py:1425 core/models.py:1425
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reaction"
msgstr "回應"
#: build/lib/core/models.py:1426 core/models.py:1426
#: build/lib/core/models.py:1944 core/models.py:1944
msgid "Reactions"
msgstr "回應"
#: build/lib/core/models.py:1436 core/models.py:1436
#: build/lib/core/models.py:1954 core/models.py:1954
msgid "email address"
msgstr "電子郵件地址"
#: build/lib/core/models.py:1455 core/models.py:1455
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitation"
msgstr "文件邀請"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1974 core/models.py:1974
msgid "Document invitations"
msgstr "文件邀請"
#: build/lib/core/models.py:1476 core/models.py:1476
#: build/lib/core/models.py:1994 core/models.py:1994
msgid "This email is already associated to a registered user."
msgstr "此電子郵件地址已與已註冊使用者關聯。"
#: build/lib/impress/settings.py:702 impress/settings.py:702
msgid "Docs AI"
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "4.5.0"
version = "4.8.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -26,7 +26,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.14.3",
"boto3==1.42.17",
"boto3==1.42.59",
"Brotli==1.2.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
@@ -34,34 +34,37 @@ dependencies = [
"django-countries==8.2.0",
"django-csp==4.0",
"django-filter==25.2",
"django-lasuite[all]==0.0.22",
"django-lasuite[all]==0.0.24",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django<6.0.0",
"django-treebeard==4.8.0",
"django-treebeard==5.0.5",
"djangorestframework==3.16.1",
"drf_spectacular==0.29.0",
"dockerflow==2024.4.2",
"dockerflow==2026.1.26",
"easy_thumbnails==2.10.1",
"factory_boy==3.3.3",
"gunicorn==23.0.0",
"jsonschema==4.25.1",
"gunicorn==25.1.0",
"jsonschema==4.26.0",
"langfuse==3.11.2",
"lxml==6.0.2",
"markdown==3.10",
"markdown==3.10.2",
"mozilla-django-oidc==5.0.2",
"nested-multipart-parser==1.6.0",
"openai==2.14.0",
"psycopg[binary]==3.3.2",
"pycrdt==0.12.44",
"PyJWT==2.10.1",
"openai==2.24.0",
"psycopg[binary,pool]==3.3.3",
"pycrdt==0.12.47",
"pydantic==2.12.5",
"pydantic-ai-slim[openai,logfire,web]==1.58.0",
"PyJWT==2.11.0",
"python-magic==0.4.27",
"redis<6.0.0",
"requests==2.32.5",
"sentry-sdk==2.48.0",
"whitenoise==6.11.0",
"sentry-sdk==2.53.0",
"uvicorn==0.41.0",
"whitenoise==6.12.0",
]
[project.urls]
@@ -74,21 +77,21 @@ dependencies = [
dev = [
"django-extensions==4.1",
"django-test-migrations==1.5.0",
"drf-spectacular-sidecar==2025.12.1",
"drf-spectacular-sidecar==2026.3.1",
"freezegun==1.5.5",
"ipdb==0.13.13",
"ipython==9.8.0",
"pyfakefs==6.0.0",
"pylint-django==2.6.1",
"ipython==9.10.0",
"pyfakefs==6.1.3",
"pylint-django==2.7.0",
"pylint<4.0.0",
"pytest-cov==7.0.0",
"pytest-django==4.11.1",
"pytest-django==4.12.0",
"pytest==9.0.2",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.25.8",
"ruff==0.14.10",
"types-requests==2.32.4.20250913",
"responses==0.26.0",
"ruff==0.15.4",
"types-requests==2.32.4.20260107",
]
[tool.setuptools]

View File

@@ -1,3 +1,5 @@
ARG FRONTEND_IMAGE=frontend-build-output
FROM node:24-alpine AS frontend-deps
# Upgrade system packages to install security updates
@@ -32,7 +34,7 @@ EXPOSE 3000
CMD [ "yarn", "dev"]
# Tilt will rebuild impress target so, we dissociate impress and impress-builder
# Tilt will rebuild impress target so, we dissociate impress and impress-builder
# to avoid rebuilding the app at every changes.
FROM impress AS impress-builder
@@ -49,6 +51,14 @@ ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
RUN yarn build
# Normalize output path to /app (matches the runtime-prod layout)
FROM scratch AS frontend-build-output
COPY --from=impress-builder /home/frontend/apps/impress/out /app
# When FRONTEND_IMAGE is set to an external image, BuildKit skips
# frontend-deps,impress-builder, and frontend-build-output entirely
FROM ${FRONTEND_IMAGE} AS frontend-source
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.22 AS frontend-production
@@ -62,9 +72,7 @@ RUN apk update && \
ARG DOCKER_USER
USER ${DOCKER_USER}
COPY --from=impress-builder \
/home/frontend/apps/impress/out \
/usr/share/nginx/html
COPY --from=frontend-source /app /app
COPY ./src/frontend/apps/impress/conf/default.conf /etc/nginx/conf.d
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint

View File

@@ -76,28 +76,6 @@ test.describe('Config', () => {
expect(webSocket.url()).toContain('ws://localhost:4444/collaboration/ws/');
});
test('it checks the AI feature flag from config endpoint', async ({
page,
browserName,
}) => {
await overrideConfig(page, {
AI_FEATURE_ENABLED: false,
});
await page.goto('/');
await createDoc(page, 'doc-ai-feature', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
expect(await page.locator('button[data-test="ai-actions"]').count()).toBe(
0,
);
});
test('it checks that Crisp is trying to init from config endpoint', async ({
page,
}) => {

View File

@@ -0,0 +1,339 @@
/* eslint-disable playwright/no-conditional-expect */
import { expect, test } from '@playwright/test';
import {
createDoc,
mockedDocument,
overrideConfig,
verifyDocName,
} from './utils-common';
import {
mockAIResponse,
openSuggestionMenu,
writeInEditor,
} from './utils-editor';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc AI feature', () => {
[
{
AI_FEATURE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: false,
selector: 'Ask AI',
},
{
AI_FEATURE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: false,
selector: 'AI',
},
].forEach((config) => {
test(`it checks the AI feature flag from config endpoint: ${JSON.stringify(config)}`, async ({
page,
browserName,
}) => {
await overrideConfig(page, config);
await page.goto('/');
await createDoc(page, 'doc-ai-feature', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
await expect(
page.getByRole('button', { name: config.selector, exact: true }),
).toBeHidden();
});
});
test('it checks the AI feature and accepts changes', async ({
page,
browserName,
}) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await mockAIResponse(page);
await page.goto('/');
await createDoc(page, 'doc-ai', browserName, 1);
await openSuggestionMenu({ page });
await page.getByText('Ask AI').click();
await expect(
page.getByRole('option', { name: 'Continue Writing' }),
).toBeVisible();
await expect(page.getByRole('option', { name: 'Summarize' })).toBeVisible();
await page.keyboard.press('Escape');
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await expect(
page.getByRole('option', { name: 'Improve Writing' }),
).toBeVisible();
await expect(
page.getByRole('option', { name: 'Fix Spelling' }),
).toBeVisible();
await expect(page.getByRole('option', { name: 'Translate' })).toBeVisible();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Accept')
.click();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
// Check Suggestion menu
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeVisible();
// Reload the page to check that the AI change is still there
await page.goto(page.url());
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
test('it reverts with the AI feature', async ({ page, browserName }) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await mockAIResponse(page);
await page.goto('/');
await createDoc(page, 'doc-ai', browserName, 1);
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello World').selectText();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await page.getByRole('option', { name: 'Translate' }).click();
await page
.getByRole('textbox', { name: 'Ask anything...' })
.fill('Translate into french');
await page.getByRole('textbox', { name: 'Ask anything...' }).press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Revert')
.click();
await expect(editor.getByText('Hello World')).toBeVisible();
});
test('it checks the AI buttons feature legacy', async ({
page,
browserName,
}) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
await route.fulfill({
json: {
answer: 'Hallo Welt',
},
});
} else {
await route.continue();
}
});
await createDoc(page, 'doc-ai', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'AI', exact: true }).click();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Rephrase' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Summarize' }),
).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'Language' }).hover();
await expect(
page.getByRole('menuitem', { name: 'English', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'French', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'German', exact: true }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'German', exact: true }).click();
await expect(editor.getByText('Hallo Welt')).toBeVisible();
});
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
browserName,
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
if (!ai_transform && !ai_translate) {
await expect(
page.getByRole('button', { name: 'AI', exact: true }),
).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI', exact: true }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_proxy: false,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai-proxy',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeHidden();
});
});

View File

@@ -41,7 +41,7 @@ test.describe('Doc Comments', () => {
// We add a comment with the first user
const editor = await writeInEditor({ page, text: 'Hello World' });
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
await page.getByRole('button', { name: 'Add comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');
@@ -124,7 +124,7 @@ test.describe('Doc Comments', () => {
// Checks add react reaction
const editor = await writeInEditor({ page, text: 'Hello' });
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
await page.getByRole('button', { name: 'Add comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');
@@ -191,7 +191,7 @@ test.describe('Doc Comments', () => {
/* Delete the last comment remove the thread */
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
await page.getByRole('button', { name: 'Add comment' }).click();
await thread.getByRole('paragraph').first().fill('This is a new comment');
await thread.locator('[data-test="save"]').click();
@@ -249,7 +249,7 @@ test.describe('Doc Comments', () => {
editor.getByText('Hello, I can edit the document'),
).toBeVisible();
await otherEditor.getByText('Hello').selectText();
await otherPage.getByRole('button', { name: 'Comment' }).click();
await otherPage.getByRole('button', { name: 'Add comment' }).click();
const otherThread = otherPage.locator('.bn-thread');
await otherThread
.getByRole('paragraph')
@@ -280,7 +280,7 @@ test.describe('Doc Comments', () => {
await expect(otherThread).toBeHidden();
await otherEditor.getByText('Hello').selectText();
await expect(
otherPage.getByRole('button', { name: 'Comment' }),
otherPage.getByRole('button', { name: 'Add comment' }),
).toBeHidden();
await otherPage.reload();
@@ -334,7 +334,7 @@ test.describe('Doc Comments', () => {
// We add a comment in the first document
const editor1 = await writeInEditor({ page, text: 'Document One' });
await editor1.getByText('Document One').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
await page.getByRole('button', { name: 'Add comment' }).click();
const thread1 = page.locator('.bn-thread');
await thread1.getByRole('paragraph').first().fill('Comment in Doc One');
@@ -388,7 +388,7 @@ test.describe('Doc Comments mobile', () => {
// Checks add react reaction
const editor = await writeInEditor({ page, text: 'Hello' });
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'Comment' }).click();
await page.getByRole('button', { name: 'Add comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');

View File

@@ -1,4 +1,3 @@
/* eslint-disable playwright/no-conditional-expect */
import path from 'path';
import { expect, test } from '@playwright/test';
@@ -7,7 +6,6 @@ import cs from 'convert-stream';
import {
createDoc,
goToGridDoc,
mockedDocument,
overrideConfig,
verifyDocName,
} from './utils-common';
@@ -39,6 +37,7 @@ test.describe('Doc Editor', () => {
.selectText();
const toolbar = page.locator('.bn-formatting-toolbar');
await expect(toolbar.getByRole('button', { name: 'Ask AI' })).toBeVisible();
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeVisible();
@@ -64,9 +63,6 @@ test.describe('Doc Editor', () => {
await expect(
toolbar.locator('button[data-test="createLink"]'),
).toBeVisible();
await expect(
toolbar.locator('button[data-test="ai-actions"]'),
).toBeVisible();
await expect(
toolbar.locator('button[data-test="convertMarkdown"]'),
).toBeVisible();
@@ -93,14 +89,12 @@ test.describe('Doc Editor', () => {
await expect(image).toHaveAttribute('role', 'presentation');
await image.dblclick();
await image.click();
await expect(toolbar.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await expect(
toolbar.locator('button[data-test="comment-toolbar-button"]'),
).toBeHidden();
await expect(
toolbar.locator('button[data-test="ai-actions"]'),
).toBeHidden();
await expect(
toolbar.locator('button[data-test="convertMarkdown"]'),
).toBeHidden();
@@ -389,139 +383,6 @@ test.describe('Doc Editor', () => {
await expect(image).toHaveAttribute('aria-hidden', 'true');
});
test('it checks the AI buttons', async ({ page, browserName }) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
await route.fulfill({
json: {
answer: 'Bonjour le monde',
},
});
} else {
await route.continue();
}
});
await createDoc(page, 'doc-ai', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'AI' }).click();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Rephrase' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Summarize' }),
).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'Language' }).hover();
await expect(
page.getByRole('menuitem', { name: 'English', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'French', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'German', exact: true }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'English', exact: true }).click();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
browserName,
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
page,
'doc-editor-ai',
browserName,
1,
);
await verifyDocName(page, randomDoc);
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI' }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
test('it downloads unsafe files', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);

View File

@@ -1,10 +1,26 @@
import { expect, test } from '@playwright/test';
import { createDoc, mockedListDocs, toggleHeaderMenu } from './utils-common';
import {
createDoc,
getGridRow,
getOtherBrowserName,
mockedListDocs,
toggleHeaderMenu,
verifyDocName,
} from './utils-common';
import { writeInEditor } from './utils-editor';
import {
addNewMember,
connectOtherUserToDoc,
updateShareLink,
} from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc grid dnd', () => {
test('it creates a doc', async ({ page, browserName }) => {
test.describe('Doc grid move', () => {
test('it checks drag and drop functionality', async ({
page,
browserName,
}) => {
await page.goto('/');
const header = page.locator('header').first();
await createDoc(page, 'Draggable doc', browserName, 1);
@@ -29,7 +45,7 @@ test.describe('Doc grid dnd', () => {
await expect(draggableElement).toBeVisible();
await expect(dropZone).toBeVisible();
// Obtenir les positions des éléments
// Get the position of the elements
const draggableBoundingBox = await draggableElement.boundingBox();
const dropZoneBoundingBox = await dropZone.boundingBox();
@@ -46,7 +62,7 @@ test.describe('Doc grid dnd', () => {
);
await page.mouse.down();
// Déplacer vers la zone cible
// Move to the target zone
await page.mouse.move(
dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2,
dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2,
@@ -161,6 +177,215 @@ test.describe('Doc grid dnd', () => {
await page.mouse.up();
});
test('it moves a doc from the doc search modal', async ({
page,
browserName,
}) => {
await page.goto('/');
const [titleDoc1] = await createDoc(page, 'Draggable doc', browserName, 1);
const otherBrowserName = getOtherBrowserName(browserName);
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Administrator', otherBrowserName);
await page
.getByRole('dialog')
.getByRole('button', { name: 'close' })
.click();
await page.getByRole('button', { name: 'Back to homepage' }).click();
const [titleDoc2] = await createDoc(page, 'Droppable doc', browserName, 1);
await page.getByRole('button', { name: 'Back to homepage' }).click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid.getByText(titleDoc1)).toBeVisible();
await expect(docsGrid.getByText(titleDoc2)).toBeVisible();
const row = await getGridRow(page, titleDoc1);
await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
await expect(
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
).toBeVisible();
const input = page.getByRole('combobox', { name: 'Quick search input' });
await input.click();
await input.fill(titleDoc2);
await expect(
page.getByRole('option').first().getByText(titleDoc2),
).toBeVisible();
// Select the first result
await page.keyboard.press('Enter');
// The CTA should get the focus
await page.keyboard.press('Tab');
// Validate the move action
await page.keyboard.press('Enter');
await expect(
page
.getByRole('dialog')
.getByText('it will lose its current access rights'),
).toBeVisible();
await page
.getByRole('dialog')
.getByRole('button', { name: 'Move', exact: true })
.first()
.click();
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
await docsGrid
.getByRole('link', { name: `Open document ${titleDoc2}` })
.click();
await verifyDocName(page, titleDoc2);
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(titleDoc1)).toBeVisible();
});
test('it proposes an access request when moving a doc without sufficient permissions', async ({
page,
browserName,
}) => {
test.slow();
await page.goto('/');
const [titleDoc1] = await createDoc(page, 'Move doc', browserName, 1);
const { otherPage, cleanup } = await connectOtherUserToDoc({
docUrl: '/',
browserName,
});
// Another user creates a doc
const [titleDoc2] = await createDoc(otherPage, 'Drop doc', browserName, 1);
await writeInEditor({
page: otherPage,
text: 'Hello world',
});
// Make it public
await otherPage.getByRole('button', { name: 'Share' }).click();
await updateShareLink(otherPage, 'Public');
await otherPage
.getByRole('dialog')
.getByRole('button', { name: 'close' })
.click();
const otherPageUrl = otherPage.url();
// The first user visit the doc to have it in his grid list
await page.goto(otherPageUrl);
await expect(page.getByText('Hello world')).toBeVisible();
await page.waitForTimeout(1000);
await page.getByRole('button', { name: 'Back to homepage' }).click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid.getByText(titleDoc1)).toBeVisible();
await expect(docsGrid.getByText(titleDoc2)).toBeVisible();
const row = await getGridRow(page, titleDoc1);
await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
await expect(
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
).toBeVisible();
const input = page.getByRole('combobox', { name: 'Quick search input' });
await input.click();
await input.fill(titleDoc2);
await expect(
page.getByRole('option').first().getByText(titleDoc2),
).toBeVisible();
// Select the first result
await page.keyboard.press('Enter');
// The CTA should get the focus
await page.keyboard.press('Tab');
// Validate the move action
await page.keyboard.press('Enter');
// Request access modal should be visible
await expect(
page
.getByRole('dialog')
.getByText(
'You need edit access to the destination. Request access, then try again.',
),
).toBeVisible();
await page
.getByRole('dialog')
.getByRole('button', { name: 'Request access', exact: true })
.first()
.click();
// The other user should receive the access request and be able to approve it
await otherPage.getByRole('button', { name: 'Share' }).click();
await expect(otherPage.getByText('Access Requests')).toBeVisible();
await expect(otherPage.getByText(`E2E ${browserName}`)).toBeVisible();
const emailRequest = `user.test@${browserName}.test`;
await expect(otherPage.getByText(emailRequest)).toBeVisible();
const container = otherPage.getByTestId(
`doc-share-access-request-row-${emailRequest}`,
);
await container.getByTestId('doc-role-dropdown').click();
await otherPage.getByRole('menuitem', { name: 'Administrator' }).click();
await container.getByRole('button', { name: 'Approve' }).click();
await expect(otherPage.getByText('Access Requests')).toBeHidden();
await expect(otherPage.getByText('Share with 2 users')).toBeVisible();
await expect(otherPage.getByText(`E2E ${browserName}`)).toBeVisible();
// The first user should now be able to move the doc
await page.reload();
await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
await expect(
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
).toBeVisible();
await input.click();
await input.fill(titleDoc2);
await expect(
page.getByRole('option').first().getByText(titleDoc2),
).toBeVisible();
// Select the first result
await page.keyboard.press('Enter');
// The CTA should get the focus
await page.keyboard.press('Tab');
// Validate the move action
await page.keyboard.press('Enter');
await expect(docsGrid.getByText(titleDoc1)).toBeHidden();
await docsGrid
.getByRole('link', { name: `Open document ${titleDoc2}` })
.click();
await verifyDocName(page, titleDoc2);
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(titleDoc1)).toBeVisible({
timeout: 15000,
});
await cleanup();
});
});
test.describe('Doc grid dnd mobile', () => {

View File

@@ -115,7 +115,7 @@ test.describe('Document grid item options', () => {
// Pin
await row.getByText(`more_horiz`).click();
await page.getByText('push_pin').click();
await page.getByRole('menuitem', { name: 'Pin' }).click();
// Check is pinned
await expect(row.getByTestId('doc-pinned-icon')).toBeVisible();
@@ -264,7 +264,7 @@ test.describe('Documents Grid', () => {
test('checks all the elements are visible', async ({ page }) => {
void page.goto('/');
let docs: SmallDoc[] = [];
let docs: SmallDoc[];
const response = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1') &&
@@ -290,7 +290,7 @@ test.describe('Documents Grid', () => {
});
test('checks the infinite scroll', async ({ page }) => {
let docs: SmallDoc[] = [];
let docs: SmallDoc[];
const responsePromisePage1 = page.waitForResponse((response) => {
return (
response.url().endsWith(`/documents/?page=1`) &&

View File

@@ -7,6 +7,7 @@ import {
mockedDocument,
verifyDocName,
} from './utils-common';
import { writeInEditor } from './utils-editor';
import {
connectOtherUserToDoc,
mockedAccesses,
@@ -20,6 +21,46 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Doc Header', () => {
test('toggles panel collapse from floating bar button', async ({
page,
browserName,
}) => {
const [docTitle] = await createDoc(
page,
'doc-floating-bar',
browserName,
1,
);
const cardCollapse = page.locator('.--docs--left-panel-collapse-button');
const collapseButton = cardCollapse.getByTestId(
'floating-bar-toggle-left-panel',
);
await expect(collapseButton).toBeVisible();
// Panel open
await expect(collapseButton).toHaveAttribute('aria-expanded', 'true');
await expect(collapseButton.getByText(docTitle)).toBeHidden();
// Collapse panel
await collapseButton.click();
await expect(collapseButton).toHaveAttribute('aria-expanded', 'false');
await expect(cardCollapse.getByText(docTitle)).toBeHidden();
// When the title is not visible in the viewport, the button should show the title
const editor = await writeInEditor({ page, text: 'Lorem ipsum' });
for (let i = 0; i < 25; i++) {
await editor.press('Enter');
}
await writeInEditor({ page, text: 'Lorem ipsum 2' });
await expect(cardCollapse.getByText(docTitle)).toBeVisible();
// Expand panel and check the title is hidden again
await collapseButton.click();
await expect(collapseButton).toHaveAttribute('aria-expanded', 'true');
await expect(cardCollapse.getByText(docTitle)).toBeHidden();
});
test('it checks the element are correctly displayed', async ({
page,
browserName,
@@ -505,7 +546,7 @@ test.describe('Doc Header', () => {
.click();
// Pin
await page.getByText('push_pin').click();
await page.getByRole('menuitem', { name: 'Pin' }).click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
@@ -526,11 +567,11 @@ test.describe('Doc Header', () => {
.click();
// Unpin
await page.getByText('Unpin').click();
await page.getByRole('menuitem', { name: 'Unpin' }).click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await expect(page.getByText('push_pin')).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible();
await page.goto('/');

View File

@@ -271,6 +271,49 @@ test.describe('Doc Tree', () => {
await expect(rootMoreOptionsButton).toBeFocused();
});
test('Shift+Tab from resize handle returns focus to selected sub-doc', async ({
page,
browserName,
}) => {
const [docParent] = await createDoc(
page,
'doc-tree-shift-tab',
browserName,
1,
);
await verifyDocName(page, docParent);
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-tree-shift-tab-child',
);
const selectedSubDoc = await getTreeRow(page, docChild);
await expect(selectedSubDoc).toHaveAttribute('aria-selected', 'true');
await selectedSubDoc.focus();
await expect(selectedSubDoc).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('Open onboarding menu')).toBeFocused();
await page.keyboard.press('Tab');
await expect(
page.locator('[data-panel-resize-handle-id]').first(),
).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(page.getByLabel('Open onboarding menu')).toBeFocused();
await page.keyboard.press('Shift+Tab');
await expect(selectedSubDoc).toBeFocused();
});
test('it updates the child icon from the tree', async ({
page,
browserName,

View File

@@ -0,0 +1,179 @@
import { expect, test } from '@playwright/test';
import {
TestLanguage,
overrideConfig,
waitForLanguageSwitch,
} from './utils-common';
test.describe('Help feature', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Onboarding modal', () => {
test('Help menu not displayed if onboarding is disabled', async ({
page,
}) => {
await overrideConfig(page, {
theme_customization: {
onboarding: {
enabled: false,
},
},
});
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Open onboarding menu' }),
).toBeHidden();
});
test('opens onboarding modal from help menu and can navigate/close', async ({
page,
}) => {
await overrideConfig(page, {
theme_customization: {
onboarding: {
enabled: true,
learn_more_url: 'https://example.com/learn-more',
},
},
});
await page.getByRole('button', { name: 'Open onboarding menu' }).click();
await page.getByRole('menuitem', { name: 'Onboarding' }).click();
const modal = page.getByTestId('onboarding-modal');
await expect(modal).toBeVisible();
await expect(page.getByTestId('onboarding-step-0')).toHaveAttribute(
'tabindex',
'0',
);
await page.getByTestId('onboarding-next').click();
await expect(page.getByTestId('onboarding-step-1')).toHaveAttribute(
'tabindex',
'0',
);
await page.getByTestId('onboarding-step-2').click();
await expect(page.getByTestId('onboarding-step-2')).toHaveAttribute(
'tabindex',
'0',
);
await page.getByTestId('onboarding-step-3').click();
await expect(page.getByTestId('onboarding-step-3')).toHaveAttribute(
'tabindex',
'0',
);
const learnMoreLink = page.getByRole('link', {
name: 'Learn more docs features',
});
await expect(learnMoreLink).toHaveAttribute(
'href',
'https://example.com/learn-more',
);
await learnMoreLink.click();
await page.getByRole('button', { name: /understood/i }).click();
await expect(modal).toBeHidden();
});
test('closes modal with Skip button', async ({ page }) => {
await page.getByRole('button', { name: 'Open onboarding menu' }).click();
await page.getByRole('menuitem', { name: 'Onboarding' }).click();
const modal = page.getByTestId('onboarding-modal');
await expect(modal).toBeVisible();
await expect(
page.getByRole('link', {
name: 'Learn more docs features',
}),
).toBeHidden();
await page.getByRole('button', { name: /skip/i }).click();
await expect(modal).toBeHidden();
});
test('Modal onboarding translated correctly', async ({ page }) => {
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);
await page
.getByRole('button', { name: "Ouvrir le menu d'embarquement" })
.click();
await page.getByRole('menuitem', { name: 'Premiers pas' }).click();
const modal = page.getByLabel('Apprenez les principes fondamentaux');
await expect(modal.getByText('Découvrez Docs')).toBeVisible();
await expect(
modal.getByText(/Rédigez votre document facilement/).first(),
).toBeVisible();
await expect(
modal.getByText(/Déplacez, dupliquez/).first(),
).toBeVisible();
await expect(
modal.getByRole('button', { name: /Passer/i }),
).toBeVisible();
await expect(
modal.getByRole('button', { name: /Suivant/i }),
).toBeVisible();
});
test('Modal is displayed automatically on first connection', async ({
page,
browserName,
}) => {
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
await expect(page.getByTestId('onboarding-modal')).toBeHidden();
await page.route(/.*\/api\/v1.0\/users\/me\//, async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
id: 'f2bfcf0b-e4b9-4153-b2e5-0d2a9a5a0a5b',
email: `user.test@${browserName.toLowerCase()}.test`,
full_name: `E2E ${browserName}`,
short_name: 'E2E',
language: 'en-us',
is_first_connection: true,
},
});
} else {
await route.continue();
}
});
let onboardingDoneCalled = false;
await page.route(
/.*\/api\/v1.0\/users\/onboarding-done\//,
async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
onboardingDoneCalled = true;
await route.continue();
}
},
);
await page.goto('/');
await expect(page.getByTestId('onboarding-modal')).toBeVisible();
await page.getByRole('button', { name: /skip/i }).click();
await expect(page.getByTestId('onboarding-modal')).toBeHidden();
expect(onboardingDoneCalled).toBeTruthy();
});
});
});

View File

@@ -98,7 +98,7 @@ test.describe('Home page', () => {
style: { width: '110px', height: 'auto' },
},
icon: {
src: '/assets/icon-docs-v2.svg',
src: '/assets/icon-docs-dsfr-v2.png',
style: {
width: '100px',
height: 'auto',

View File

@@ -9,7 +9,13 @@ export type BrowserName = 'chromium' | 'firefox' | 'webkit';
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
export const CONFIG = {
AI_BOT: {
name: 'Docs AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_FEATURE_BLOCKNOTE_ENABLED: true,
AI_FEATURE_LEGACY_ENABLED: true,
API_USERS_SEARCH_QUERY_MIN_LENGTH: 3,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Page } from '@playwright/test';
export const getEditor = async ({ page }: { page: Page }) => {
@@ -30,3 +32,55 @@ export const writeInEditor = async ({
.fill(text);
return editor;
};
export const mockAIResponse = async (page: Page) => {
await page.route(/.*\/ai-proxy\//, async (route) => {
const req = route.request();
if (req.method() !== 'POST') {
return route.continue();
}
// Extract the block ID from the request's selectedBlocks
const requestData = req.postDataJSON();
const messages = requestData?.messages || [];
const userMessage = messages.find((msg: any) => msg.role === 'user');
const documentState = userMessage?.metadata?.documentState;
const selectedBlocks = documentState?.selectedBlocks || [];
const blockId = selectedBlocks[0]?.id || 'initialBlockId$';
const sse = [
`data: {"type":"start"}\n\n`,
`data: {"type":"start-step"}\n\n`,
`data: ${JSON.stringify({
type: 'tool-input-available',
toolCallId: 'chatcmpl-mock-0',
toolName: 'applyDocumentOperations',
input: {
operations: [
{
type: 'update',
id: blockId,
block: '<p>Bonjour le monde</p>',
},
],
},
})}\n\n`,
`data: {"type":"finish-step"}\n\n`,
`data: {"type":"finish","finishReason":"tool-calls"}\n\n`,
`data: [DONE]\n\n`,
].join('');
await route.fulfill({
status: 200,
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'x-vercel-ai-data-stream': 'v1',
'x-accel-buffering': 'no',
Connection: 'keep-alive',
},
body: sse,
});
});
};

View File

@@ -8,12 +8,6 @@ const eslintConfig = defineConfig([
docs: docsPlugin,
},
extends: ['docs/playwright'],
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: ['./tsconfig.json'],
},
},
},
]);

View File

@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "4.5.0",
"version": "4.8.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -15,7 +15,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.57.0",
"@playwright/test": "1.58.2",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-plugin-docs": "*",

View File

@@ -12,6 +12,7 @@
# next.js
/.next/
/out/
next-env.d.ts
# production
/build

View File

@@ -3,7 +3,7 @@ server {
listen 3000;
server_name localhost;
root /usr/share/nginx/html;
root /app;
location / {
try_files $uri index.html $uri/index.html =404;

View File

@@ -18,6 +18,12 @@ const themeWhiteLabelLight = getUIKitThemesFromGlobals(whiteLabelGlobals, {
'3xs': '0.25rem',
'2xs': '0.375rem',
},
font: {
families: {
base: 'Inter Variable, Roboto Flex Variable, sans-serif',
accent: 'Inter Variable, Roboto Flex Variable, sans-serif',
},
},
},
},
});
@@ -33,8 +39,8 @@ const themesDSFRLight = getUIKitThemesFromGlobals(dsfrGlobals, {
globals: {
font: {
families: {
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
base: 'Marianne, Inter Variable, Roboto Flex Variable, sans-serif',
accent: 'Marianne, Inter Variable, Roboto Flex Variable, sans-serif',
},
},
},

View File

@@ -2,17 +2,14 @@ import { defineConfig } from '@eslint/config-helpers';
import docsPlugin from 'eslint-plugin-docs';
const eslintConfig = defineConfig([
{
ignores: ['.next/**', 'out/**', 'public/service-worker.js'],
},
{
plugins: {
docs: docsPlugin,
},
extends: ['docs/next'],
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
project: ['./tsconfig.json'],
},
},
settings: {
next: {
rootDir: import.meta.dirname,

View File

@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View File

@@ -1,7 +1,5 @@
const crypto = require('crypto');
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
@@ -17,10 +15,27 @@ const nextConfig = {
// Enables the styled-components SWC transform
styledComponents: true,
},
experimental: {
// Tree-shake barrel files for these packages so webpack only bundles the
// symbols that are actually imported, reducing chunk sizes noticeably for
// Mantine and the Cunningham design system.
optimizePackageImports: ['@mantine/core', '@mantine/hooks', 'lodash'],
},
generateBuildId: () => buildId,
env: {
NEXT_PUBLIC_BUILD_ID: buildId,
},
/**
* In dev mode, Next.js doesn't use Webpack, but Turbopack.
*/
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
webpack(config, { isServer }) {
// Grab the existing rule that handles SVG imports
const fileLoaderRule = config.module.rules.find((rule) =>
@@ -43,22 +58,6 @@ const nextConfig = {
},
);
// Copy necessary fonts from node_modules to public directory during build or dev
config.plugins.push(
new CopyPlugin({
patterns: [
{
from: path.resolve(
__dirname,
'../../node_modules/emoji-datasource-apple/img/apple/64',
),
to: path.resolve(__dirname, 'public/assets/fonts/emoji'),
force: true,
},
],
}),
);
if (!isServer && process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true') {
config.plugins.push(
new InjectManifest({

View File

@@ -1,17 +1,21 @@
{
"name": "app-impress",
"version": "4.5.0",
"version": "4.8.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev",
"build": "prettier --check . && yarn stylelint && next build",
"predev": "node scripts/prebuild.mjs",
"build": "prettier --check . && yarn stylelint && next build --webpack",
"prebuild": "node scripts/prebuild.mjs",
"postbuild": "node scripts/postbuild.mjs",
"build:ci": "cp .env.development .env.local && yarn build",
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes && yarn prettier && yarn stylelint --fix",
"start": "npx -y serve@latest out",
"lint": "tsc --noEmit && next lint",
"lint": "tsc --noEmit && eslint .",
"lint:verbose": "cross-env DEBUG=\"eslint:cli-engine,eslint:linter\" eslint .",
"prettier": "prettier --write .",
"stylelint": "stylelint \"**/*.css\"",
"test": "vitest",
@@ -19,31 +23,34 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.46.2",
"@blocknote/core": "0.46.2",
"@blocknote/mantine": "0.46.2",
"@blocknote/react": "0.46.2",
"@blocknote/xl-docx-exporter": "0.46.2",
"@blocknote/xl-multi-column": "0.46.2",
"@blocknote/xl-odt-exporter": "0.46.2",
"@blocknote/xl-pdf-exporter": "0.46.2",
"@ai-sdk/openai": "3.0.19",
"@blocknote/code-block": "0.47.1",
"@blocknote/core": "0.47.1",
"@blocknote/mantine": "0.47.1",
"@blocknote/react": "0.47.1",
"@blocknote/xl-ai": "0.47.1",
"@blocknote/xl-docx-exporter": "0.47.1",
"@blocknote/xl-multi-column": "0.47.1",
"@blocknote/xl-odt-exporter": "0.47.1",
"@blocknote/xl-pdf-exporter": "0.47.1",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-outlined": "5.2.31",
"@fontsource-variable/material-symbols-outlined": "5.2.35",
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/cunningham-react": "4.1.0",
"@gouvfr-lasuite/cunningham-react": "4.2.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.18.7",
"@hocuspocus/provider": "3.4.3",
"@mantine/core": "8.3.12",
"@mantine/hooks": "8.3.12",
"@gouvfr-lasuite/ui-kit": "0.19.6",
"@hocuspocus/provider": "3.4.4",
"@mantine/core": "8.3.14",
"@mantine/hooks": "8.3.14",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.34.0",
"@tanstack/react-query": "5.90.18",
"@sentry/nextjs": "10.38.0",
"@tanstack/react-query": "5.90.21",
"@tiptap/extensions": "*",
"ai": "6.0.49",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -51,56 +58,56 @@
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "25.7.4",
"i18next-browser-languagedetector": "8.2.0",
"i18next": "25.8.12",
"i18next-browser-languagedetector": "8.2.1",
"idb": "8.0.3",
"lodash": "4.17.23",
"luxon": "3.7.2",
"next": "15.5.9",
"posthog-js": "1.325.0",
"next": "16.1.6",
"posthog-js": "1.347.2",
"react": "*",
"react-aria-components": "1.14.0",
"react-aria-components": "1.15.1",
"react-dom": "*",
"react-dropzone": "14.3.8",
"react-i18next": "16.5.3",
"react-dropzone": "15.0.0",
"react-i18next": "16.5.4",
"react-intersection-observer": "10.0.2",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
"styled-components": "6.3.8",
"styled-components": "6.3.9",
"use-debounce": "10.1.0",
"uuid": "13.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
"zustand": "5.0.10"
"zod": "3.25.28",
"zustand": "5.0.11"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.91.2",
"@tanstack/react-query-devtools": "5.91.3",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@types/lodash": "4.17.23",
"@types/luxon": "3.7.1",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@vitejs/plugin-react": "5.1.2",
"copy-webpack-plugin": "13.0.1",
"@vitejs/plugin-react": "5.1.4",
"cross-env": "10.1.0",
"dotenv": "17.2.3",
"dotenv": "17.3.1",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "27.4.0",
"jsdom": "28.1.0",
"node-fetch": "2.7.0",
"prettier": "3.8.0",
"prettier": "3.8.1",
"stylelint": "16.26.1",
"stylelint-config-standard": "39.0.1",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vite-tsconfig-paths": "6.0.4",
"vitest": "4.0.17",
"webpack": "5.104.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.0.18",
"webpack": "5.105.2",
"workbox-webpack-plugin": "7.1.0"
},
"packageManager": "yarn@1.22.22"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 MiB

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