Compare commits

...

116 Commits

Author SHA1 Message Date
Anthony LC 9135dff088 🔖(minor) release 3.6.0
Added:
- 👷(CI) add bundle size check job
- (frontend) use title first emoji as doc icon in tree

Changed:
- ♻️(docs-app) Switch from Jest tests to Vitest
- (frontend) improve accessibility:
  - 🌐(frontend) set html lang attribute dynamically
  - (frontend) inject language attribute to pdf export
  - (frontend) improve accessibility of search modal
  - (frontend) add correct attributes to decorative and interactive icons
  - 🎨(frontend) improve nav structure
  - ️(frontend) keyboard interaction with menu
  - (frontend) improve header accessibility
  - (frontend) improve accessibility for decorative images in editor
- ♻️(backend) fallback to email identifier when no name
- 🐛(backend) allow ASCII characters in user sub field
- ️(frontend) improve fallback width calculation

Fixed:
- 🐛(makefile) Windows compatibility fix for Docker volume mounting
- 🐛(minio) fix user permission error with Minio and Windows
- 🐛(frontend) fix export when quote block and inline code
- 🐛(frontend) fix base64 font
- 🐛(backend) allow editor to delete subpages
- 🐛(frontend) fix dnd conflict with tree and Blocknote
- 🐛(frontend) fix display bug on homepage
2025-09-05 09:11:58 +02:00
Anthony LC cc4c67d15b 🩹(frontend) add computed_link_reach on PUT link-configuration
By default a document is "restricted", a restricted
document cannot have a role "editor" or "reader".
With inheritance, a child document could have a computed
link reach different than "restricted" though.

We pass the computed link reach when we update the
link role, to be sure if follows the parent computed
link reach.
2025-09-05 09:11:58 +02:00
Anthony LC 63a2bde11e (e2e) fix e2e tests
After translating to french, a selector was not
accessible anymore because the aria label
was in english. We update the selector to use the
french aria label.
We increase as well the timeout of another test
that was flaky.
2025-09-04 16:15:08 +02:00
Anthony LC b317a2a596 🌐(i18n) update translated strings
Update translated files with new translations
2025-09-04 13:56:23 +02:00
Anthony LC 39ef6d10ff 🐛(frontend) fix display bug on homepage
A section in the homepage was not displaying
correctly anymore. This commit fixes the issue.
2025-09-04 10:44:35 +02:00
Anthony LC 961ae3c39e 🐛(frontend) remove padding from ul in left panel favorites
We recently change from a div to a ul for better semantics.
ul include by default padding, we remove it to
align with the design.
2025-09-04 10:27:53 +02:00
Anthony LC 726b50d6b5 ️(frontend) improve fallback width calculation
Sometimes we do not have the width of some
columns in a table. In such cases, we need to
calculate a fallback width to ensure the table
is rendered correctly.
We were previously using 120 points as the
fallback width, but this has been improved
to better fit the content.
We now check the size left and distribute it
among the unknown columns.
2025-09-03 18:11:43 +02:00
Anthony LC 814eb1f1a1 🐛(frontend) fix modal backdrop z-index
Recent upgrade of ui-kit removed the z-index for
the modal backdrop, causing it to be hidden behind
other elements. This commit restores the z-index
 to ensure the modal backdrop is displayed correctly.
2025-09-03 10:21:27 +02:00
Anthony LC 648528499c 🐛(frontend) fix dnd conflict with tree and Blocknote
We have different DND system in the page, one on the
menu tree and one in the Blocknote editor.
The menu tree was adding a transparent layer
when user were dragging element on the Blocknote
editor, blocking the Blocknote DND.
We update the ui-kit to add the prop dndRootElement,
dndRootElement is now used to specify the root element
for DND, this transparent layer is now only applied
when a drag is made from the menu tree.

It stabilize as well the drop position, making it easier to drop element.
2025-09-03 10:21:27 +02:00
Anthony LC 474e5ac0c0 🐛(frontend) fix background color during dnd
When we were dragging an item in the doc tree,
the background color was opaque, making it difficult
to see the underlying content. The cause was
that we were overriding the transparent background
color.
2025-09-03 10:21:27 +02:00
Henry Hiles a799d77643 📝(doc) add Federated Nexus docs instance (#1320)
## Purpose

Adds a new Docs instance, as suggested here:
https://github.com/suitenumerique/docs/discussions/1316

## 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`
- [ ] I have added a changelog entry under `## [Unreleased]` section (if
noticeable change)
- [ ] I have added corresponding tests for new features or bug fixes (if
applicable)

Signed-off-by: Henry-Hiles <henry@henryhiles.com>
2025-09-02 14:38:52 +00:00
Manuel Raynaud 2e04b63d2d 🐛(backend) allow creator to delete subpages
An editor who created a subpages should be allowed to delete it.
We change the abilities to be coherent between the creation and the
deletion.
Fixes #1193
2025-09-01 22:43:44 +02:00
Anthony LC eec419bdba 🐛(renovate) fix renovate
Fix renovate configuration:
- schedule at 7am the monday
- avoid wiping manual fixes unless needed
- only create during schedule
2025-09-01 17:05:49 +02:00
Anthony LC baa5630344 🐛(CI) fix npm fallback on bundle-size-check
Adding install-script to ensure Yarn is used explicitly
during compressed-size-action to prevent npm peer conflict.
2025-09-01 17:05:49 +02:00
Anthony LC e7b551caa4 🍱(frontend) update material-icons font
We don't use the material-icons font from
ui-kit anymore, we need to import it directly
in order to use both the outlined and filled variants.
2025-09-01 17:05:48 +02:00
Anthony LC 4dfc1584bd ️(frontend) add back Marianne font
Marianne font was in base64 in the ui-kit,
it was not an optimize way to do it.
We do not have a CDN yet so the best
is to put them back in the project in
waiting for a CDN options.
2025-09-01 17:05:48 +02:00
renovate[bot] 09eddfc339 ⬆️(dependencies) update js dependencies 2025-09-01 17:05:48 +02:00
Olivier Laurendeau 75f2e547e0 🔧(frontend) increase test-e2e-other-browser action timeout
It was failing at 20min, increase the timeout to 30 min
2025-09-01 16:35:17 +02:00
Olivier Laurendeau d1cbdfd819 (frontend) use title first emoji as doc icon in tree
Implemented emoji detection system, new DocIcon component.
2025-09-01 16:35:17 +02:00
Manuel Raynaud 0b64417058 🔧(ci) always run all git-lint steps (#1323)
git-lint steps are independant and we would like to have all checks at
once. Using the `if: always()` instruction should ensure all steps
should be run event if the previous fails.
2025-09-01 12:29:47 +00:00
Anthony LC 57a505a80c ⬆️(dependencies) Bump next from 15.4.6 to 15.4.7
Bumps [next](https://github.com/vercel/next.js) from 15.4.6
to 15.4.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.4.6...v15.4.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.4.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 11:56:40 +02:00
Anthony LC 21ee38c218 🐛(frontend) fix export when quote block and inline code
When exporting documents, if a inline code was inside
a quote block, the PDF export was failing because the
inline code was searching the GeistMono font in
italics, which was not available.
We switch to the core "Courier" font for code marks,
which is available in italics.
2025-08-29 18:42:27 +02:00
Manuel Raynaud 09de014a43 🐛(back) allow ASCII characters in user sub field
All ASCII characters are allowed in a sub, we change the sub validator
to reflect this.
2025-08-29 13:59:06 +00:00
Anthony LC 8d42149304 👷(CI) trigger bundle-size-check only if necessary
We don't need to run the bundle-size-check job if
the app didn't change.
If the yarn.lock file or the app have changed, the
bundle-size-check job will be triggered.
2025-08-29 10:21:13 +02:00
Maximilian Bosch 2451a6a322 📦️(mail) make DIR_MAILS variable configurable
That allows downstream packages to e.g. split the builds of
frontend/backend/mail, as it's the case in nixpkgs:
https://github.com/NixOS/nixpkgs/commit/5597a4f8f9f82e6a4829f3527728016c07838f4f
2025-08-29 08:00:43 +00:00
Manuel Raynaud d5c9eaca5a ♻️(backend) fallback to email identifier when no name (#1298)
In the UserlightSerializer, if the user has no short_name or full_name,
we have no info about the user. We decided to use the email identifier
and slugify it to have a little bit information.
2025-08-29 07:39:55 +00:00
Loïc Tosser 1491012969 🔧(project) update OIDC_OP_LOGOUT_ENDPOINT to match keycloak
Update default value of OIDC_OP_LOGOUT_ENDPOINT
to match keycloak.
2025-08-28 17:06:07 +02:00
Karl Horky 9dcf478dd3 ✏️(frontend) fix GitHub capitalization
The capitalization of the product GitHub has a capital H

Signed-off-by: Karl Horky <karl.horky@gmail.com>
2025-08-28 16:44:28 +02:00
Manuel Raynaud 586825aafa ♻️(back) stop returning a 500 on cors_proxy on request failure
On the cors_proxy endpoint, if the fetched url fails we were returning
an error 500. Instead, we log the exception and return a 400 to not
give back information to the frontend application.
2025-08-28 16:06:35 +02:00
Manuel Raynaud 247550fc13 ♻️(back) validate url used in cors_proxy endpoint
The url used by the cors_proxy was not validated, other value than a
http url can be used. We use the built in URLValidator to validate it is
a valid url.
2025-08-28 16:06:35 +02:00
Manuel Raynaud 781c85b66b ️(ci) use setup-python cache option
The setup-python action is able to cache the dependencies and reuse this
cache while the pyproject file has not changed. It is easy to setup,
just the package manager used has to be declared in the cache settings.
2025-08-28 14:00:16 +00:00
Anthony LC 64f967cd29 (frontend) assert DocToolBox depends the licence
Thanks to Vitest we can now assert more complicated
parts of the code without too much mocking.
2025-08-28 11:20:12 +02:00
Anthony LC 1eee24dc19 ♻️(docs-app) Switch from Jest tests to Vitest
We have migrated the testing framework from Jest
to Vitest for the Docs application.
This change includes updates to test files,
configuration files, and the addition of new setup
files for Vitest.
2025-08-28 11:11:29 +02:00
Anthony LC ff9e13ca03 👷(CI) add bundle size check job
Job that will give a report on the bundle size
of the frontend application.
Good to know if the bundle size is increasing or
decreasing and if the changes are acceptable.
2025-08-28 10:37:54 +02:00
Anthony LC 7758e64f40 💄(makefile) add information messages during bootstrap
Add nicely formatted messages to the Makefile to
indicate the start and end of the bootstrap process.
This will help users understand when the bootstrap
process begins and ends, improving the
overall user experience.
2025-08-28 10:16:14 +02:00
virgile-dev 4ab9edcd57 📝(doc) update readme and add community installation methods (#1292)
## Purpose

To showcase the work of the community to make docs deployment easier


## Proposal

- [x] Added README.md in /docs/installation with methods contributed by
the community
- [x] Added a list of public instances on the main README.md
- [x] Updated the warnings around Blocknote XL packages from AGPL to GPL

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`
- [ ] I have added a changelog entry under `## [Unreleased]` section (if
noticeable change)
- [ ] I have added corresponding tests for new features or bug fixes (if
applicable)

---------

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-08-26 16:03:39 +03:00
Luca Weiss 0892c05321 📝(compose) Increase attachment upload size to 10 MB
Mostly give this as an example how a person deploying this knows which
knob to turn.

Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-08-18 19:02:51 +02:00
Luca Weiss 2375bc136c 📝(compose) Whitespace cleanup in default.conf.template
Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-08-18 19:02:50 +02:00
Luca Weiss e1c2053697 📝(compose) Remove double colon from yaml
It doesn't look like this is supposed to be there.

Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-08-18 19:02:49 +02:00
Luca Weiss 58f68d86e1 📝(compose) Reindent compose.yaml files
Follow yamlllint's suggestions and use the correct indentation for
lists.

Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-08-18 19:02:48 +02:00
Luca Weiss 7c97719907 📝(compose) Update deprecated USER_OIDC_FIELD* variables in example
Signed-off-by: Luca Weiss <luca@lucaweiss.eu>
2025-08-18 19:02:40 +02:00
Cyril d0c9de9d96 (frontend) set empty alt for decorative images in blocknote editor
ensure decorative images have empty alt to comply with RGAA 1.2 accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-14 14:39:42 +02:00
Cyril 81f3997628 (frontend) improve accessibility of search modal for screen readers
added clearer sr-only translations and aria-hidden for non-essential content

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-08 08:58:22 +02:00
Anthony LC 0cf8b9da1a 🐛(minio) fix user permission error with Minio and Windows
With Minio Docker and Windows, the user ID needs
 to be set to `0:0` to avoid permission issues.
 This change ensures that the Minio container
 runs with root privileges on Windows, which
 is necessary for proper file access and management.
2025-08-07 12:37:00 +02:00
Anthony LC 7be761ce84 🐛(makefile) Windows compatibility fix for Docker volume mounting
On Windows systems, Docker volume paths starting
with a single / can be interpreted incorrectly
by the Docker daemon. The double slash (//) helps
Docker on Windows properly interpret the path as
an absolute path within the container, ensuring
that the working directory is correctly set
when running mail-related yarn commands.
2025-08-07 12:36:29 +02:00
Cyril 5181bba083 ️(a11y) improve keyboard access for language menu and action buttons
Enhances nav for language switch and makes DocsGridActions buttons accessible

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-07 11:58:47 +02:00
Anthony LC f434d78b5d ⬆️(dependencies) update js dependencies
- Update js dependencies
- Fix linters
2025-08-07 11:06:34 +02:00
Cyril e07f709dd4 (frontend) improve accessibility of global docs home link at top
moved aria-label and added aria-hidden for better accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-06 15:37:51 +02:00
Cyril G afbacb0a24 ️(frontend) improve left panel accessibility (#1262)
Improve overall accessibility of the left panel:
- ️(frontend) make LeftPanelTargetFilter accessible and use Box as nav
- ️(frontend) improve accessibility in left panel components
- (e2e) fix e2e test to expect aria-current instead of aria-selected
- (frontend) add semantic ul/li to LeftPanel
- (frontend) improve favorite item a11y and update e2e test accordingly
2025-08-06 14:20:53 +02:00
Anthony LC 409e073192 🤡(e2e) mock PATCH language switch
We add some flaky tests because the aria label
selectors were not everytime in english language.
It was because the language switch was not mocked
in the e2e tests, impacting the consistency of
other concurrent tests.
We mock the language switch in the e2e tests
to ensure that the other tests are not impacted
by the language switch.
2025-08-05 12:42:13 +02:00
Jan Conen 886dcb75d5 📝(self-hosted) commands copy-pastable
Make bash commands copy-pastable by prepending
the foldername to the commands.

Signed-off-by: Jan Conen <janconen@hotmail.com>
2025-08-05 11:47:44 +02:00
Jan Conen bb4d2a9fea 📝(self-hosted) default.conf.template when using nginx-proxy
Add step to copy default.conf.template
hen using nginx-proxy.

Signed-off-by: Jan Conen <janconen@hotmail.com>
2025-08-05 11:47:08 +02:00
Moritz Schlarb 5e5054282e 📝(doc) Fix commands in docker compose section
Corrected the commands in step 1

Signed-off-by: Moritz Schlarb <schlarbm@uni-mainz.de>
2025-08-05 11:14:40 +02:00
timo f497e75426 🔧(project) Add trailing slash to yprovider api path
The value in the production environment .env example was missing a
trailing slash in the path. This commit adjusts this to be in the same
format as in other places.
2025-08-05 10:02:31 +02:00
Cyril 97ab13ded6 (e2e) fix broken e2e tests by updating selectors
selectors were updated to stabilize and fix the failing e2e tests

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 16:00:17 +02:00
Cyril 99d674c615 ️(frontend) add correct attributes to decorative and interactive icons
Add aria-hidden and aria-label to improve screen reader accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 13:35:48 +02:00
Cyril 1cdb6b62c8 (e2e) ensure i18n.language is injected into generated PDF
Adds an end-to-end test to verify language injection in the generated PDF.

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 09:35:13 +02:00
Cyril 2bf53301d2 ️(frontend) inject language attribute to pdf export
added language="fr-FR" to <Document /> in ModalExport.tsx via cloneElement()
to improve accessibility and ensure correct screen reader pronunciation

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 09:35:13 +02:00
Cyril ec84f31bc7 ️(frontend) set html lang attribute dynamically based on current loc
ensures proper language tag is set for accessibility and SEO compliance

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-08-04 08:42:56 +02:00
rouja 7813219b86 ♻️(documentation) remove unused environment variables
Yesterday during a deployment, we discovered that these variables are
unused:
POSTGRES_DB
POSTGRES_USER
POSTGRES_PASSWORD
2025-08-01 12:42:02 +00:00
Anthony LC cecb4f5756 🔖(minor) release 3.5.0
Added:
- (helm) Service Account support for K8s Resources in Helm Charts
- (backend) allow masking documents from the list view
- (frontend) subdocs can manage link reach
- (frontend) add duplicate action to doc tree
- (frontend) Interlinking doc
- (frontend) add multi columns support for editor

Changed:
- ♻️(frontend) search on all docs if no children
- ♻️(frontend) redirect to doc after duplicate
- 🔧(project) change env.d system by using local files
- ️(frontend) improve tree stability
- ️(frontend) improve accessibility
- 🛂(frontend) block drag n drop when not desktop

Fixed:
- 🐛(service-worker) Fix useOffline Maximum update depth exceeded
- 🐛(frontend) fix empty left panel after deleting root doc
- 🐛(helm) charts generate invalid YAML for collaboration API / WS
- 🐛(frontend) 401 redirection overridden
- 🐛(frontend) include root parent in search
2025-08-01 09:45:02 +02:00
Anthony LC 63efe40a7b 🐛(frontend) fix interlinking click with Firefox
Fixed Firefox requiring double-click on
interlinks by adding draggable="false" to prevent
drag detection conflicts in contenteditable areas.
2025-08-01 09:45:02 +02:00
AntoLC e26c3dff35 🌐(i18n) update translated strings
Update translated files with new translations
2025-07-31 14:54:06 +02:00
Anthony LC f5f9d8a877 (frontend) interlinking export
Create interlinking link mapping for docx and pdf export.
2025-07-31 13:26:09 +02:00
Anthony LC e7709badbb (frontend) create editor shortcuts hook
We created the editor shortcuts hook to handle
the shortcuts for the editor.
We implemented the following shortcuts:
- "@" to open the interlinking inline content
2025-07-31 13:26:09 +02:00
Anthony LC 2a7c0ef800 (frontend) create page from dropdown search
We are now able to create a new page from
the dropdown search.
2025-07-31 13:26:09 +02:00
Anthony LC 155e7dfe22 (frontend) interlinking custom inline content
We want to be able to interlink documents in the editor.
We created a custom inline content that allows
users to interlink documents.
2025-07-31 13:00:11 +02:00
Anthony LC afa48b6675 (frontend) create page from slash menu
We are now able to create a new page from
the slash menu.
2025-07-31 12:57:25 +02:00
Anthony LC f12d30cffa 🚚(frontend) reduce features coupling
Move some components and assets to `doc-management`
to reduce coupling between features:
- SimpleDocItem from `doc-grid` to `doc-management`
- useCreateChildDoc from `doc-tree` to `doc-management`
- isOwnerOrAdmin from `doc-tree` to `doc-management`
2025-07-30 15:11:37 +02:00
Anthony LC 30dfea744a 🐛(frontend) include root parent in search
When searching for documents, the root parent
document is now included in the search
results if it matches the search query.
2025-07-30 14:56:30 +02:00
Anthony LC 2cbe363a5f 🛂(frontend) block drag n drop when not desktop
Scrolling on mobile devices was causing issues
with drag and drop functionality, documents were
being moved unintentionally.
This commit disables drag and drop on mobile devices
to prevent this issue.
2025-07-30 14:06:39 +02:00
Anthony LC 7f450e8aa8 ⬆️(frontend) Bump linkifyjs from 4.3.1 to 4.3.2
Bumps linkifyjs from 4.3.1 to 4.3.2.

---
updated-dependencies:
- dependency-name: linkifyjs
  dependency-version: 4.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 13:21:02 +02:00
Cyril 7021c0f849 (changelog) add accessibility note to CHANGELOG (#1232)
updating Changelog.md with accessibility improvement
2025-07-28 18:07:44 +02:00
Cyril e8d18d85e9 ️(frontend) improve contrast for links
Updated anchor link color from greyscale-500 to greyscale-600
2025-07-28 17:55:02 +02:00
AlexB 67a195f89c (helm) add serviceAccountName parameter for services
Add support for specifying custom service accounts
in all Kubernetes resources in our Helm charts
to enable workload identity federation with managed
cloud services (PostgreSQL, Redis, etc.).
This allows deployments to authenticate to cloud
resources without embedding credentials in secrets.
2025-07-28 09:18:12 +02:00
renovate[bot] 09b6fef63f ⬆️(dependencies) update js dependencies 2025-07-25 13:59:03 +00:00
Anthony LC 11d0bafc94 (frontend) add multi columns support for editor
We add multi columns support for editor,
now you can add columns to your document.
Works with export.
📄AGPL feature.
2025-07-25 15:27:01 +02:00
Anthony LC 1ae831cabd ♻️(frontend) search on all docs if no children
When searching for documents, if no children are
found, the search will now include all documents
instead of just those with children.
2025-07-25 14:30:18 +02:00
Manuel Raynaud f1c2219270 🔧(chore) replace old repo url to suitenumerique org
The old repo url on the numerique-gouv orga was still present in the
repo. This commit replaces them to the current repo url.
2025-07-25 12:15:17 +00:00
Anthony LC 8c9380c356 🐛(frontend) fix empty left panel after deleting root doc
When we were deleting a root document, the left panel
was getting empty. It was because the panel thought that
it was a child document and was trying clear
dynamically the panel.
Now, we are checking if the document is a root or not,
if it is a root we just redirect to the homepage.
2025-07-25 12:55:29 +02:00
Anthony LC 3ff6d2541c ♻️(frontend) use more reliable properties in useTreeUtils
Using the treeContext was causing issues with
the current parent detection, in many places
the context is not available.
"depth" property is more reliable than
"nb_accesses_ancestors".
2025-07-25 12:22:48 +02:00
Anthony LC 34ce276222 (frontend) subdocs can manage link reach
The subdocs can now have their own link reach
properties, dissociated from the parent document.
2025-07-25 12:22:47 +02:00
Anthony LC 04273c3b3e 🐛(frontend) redirection 401 overridden
To capture a 401 we were using "onError" in the
queryClient default mutation options. The problem
is this way does not capture globally the onError,
if a mutation uses as well is own "onError", it will
override the default one, causing the 401 to
not be captured anymore.
We now use MutationCache, which allows us to
capture globally the onError, even if a mutation
has its own "onError" defined, this global one will
still be called.
2025-07-25 12:03:43 +02:00
Samuel Paccoud - DINUM 0b301b95c8 (backend) allow masking documents from the list view
Once users have visited a document to which they have access,
they can't remove it from their list view anymore. Several
users reported that this is annoying because a document that
gets a lot of updates keeps popping up at the top of their list
view.

They want to be able to mask the document in a click. We propose
to add a "masked documents" section in the left side bar where the
masked documents can still be found.
2025-07-24 18:39:56 +02:00
Samuel Paccoud - DINUM 228bdf733e (backend) fix wrong docstrings in tests for favorite documents
This was most likely due to copy pasta fail.
2025-07-24 18:39:56 +02:00
Anthony LC bbf48f088f ️(frontend) improve tree stability
Improve tree stability by limiting the requests,
we now only load the tree request one time then
we let the treeContext handle the state without
mutating it directly.
We do not do the doc subpage request anymore,
the treeContext has already the data we need,
we just need to update the tree node when needed.
2025-07-24 13:29:26 +02:00
Anthony LC b28ff8f632 🚨(frontend) fix lint warning about unnecessary assertion
- Recent upgrade highlighted a lint warning about
an unnecessary assertion in the BlockNoteToolbar component.
This commit removes the assertion to resolve the warning.
- Fix a test - upgrade causes an error on a selector click
2025-07-24 12:38:31 +02:00
Anthony LC 14b7cdf561 ⬆️(dependencies) update js dependencies 2025-07-23 16:32:07 +02:00
Krzysztof Cybulski c534fed196 📝(typo) fix link to kubernetes.md in compose.md
Link to kubernetes.md in compose.md was incorrect,
it was pointing to a non-existing file.

Signed-off-by: Krzysztof Cybulski <k.cybulski.dev@tuta.io>
2025-07-23 13:27:14 +02:00
Anthony LC c1a740b7d4 ⬆️(dependency) Bump form-data from 4.0.2 to 4.0.4
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.2 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.2...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 12:20:08 +02:00
Anthony LC 83f2b3886e (frontend) add duplicate action to doc tree
We added a duplicate action to the document tree.
2025-07-21 19:48:58 +02:00
Anthony LC 966e514c5a ♻️(frontend) redirect to doc after duplicate
When we duplicate a document from a document page,
we now redirect the user to the newly created
document.
2025-07-21 19:48:57 +02:00
Anthony LC ef6d6c6a59 🏗️(e2e) cleaning and more consistant naming
Clean up e2e tests by removing unused utils
and renaming some files for consistency.
2025-07-21 18:07:10 +02:00
Anthony LC e79f3281b1 🐛(frontend) fix unfold subdocs not clickable at the bottom
At the bottom of the tree panel, the subdocs
were not clickable due to a CSS issue.
This commit adjusts the CSS to ensure that
the subdocs can be unfolded properly.
2025-07-21 17:34:44 +02:00
Anthony LC b78550b513 💄(frontend) visibility icon near title
It was decided to add a visibility icon near the
title of the document in the grid view.
2025-07-21 16:28:17 +02:00
Anthony LC 5a23c97681 🐛(service-worker) Fix useOffline Maximum update depth exceeded
Sentry was reporting a "Maximum update depth exceeded" error
comming from the `useOffline` hook. We updated the hook to
avoid mutation. Seems to impact mainly edge browsers.
2025-07-21 16:05:34 +02:00
Anthony LC 040eddbe6b 🔧(project) change env.d system by using local files
We had lot of problems with the previous env.d system.
Users were often confused by the need to change
the env.d files manually, leading to issues
when using the project locally.
This commit introduces a new system that uses
.env.local files, which are automatically created
and can be modified by users without affecting
the original env.d files. This should simplify
the development process and reduce confusion by
removing the need to manually edit env.d files.
2025-07-21 15:44:52 +02:00
Richard Jones f2e54308d2 🐛(helm) charts generate invalid YAML for collaboration API / WS
Closes #890

Remove the service blocks outside the conditionals in the collaboration
API and WS templates.

Signed-off-by: Richard Jones <rich@linuxplicable.org>
2025-07-18 14:22:03 +02:00
Anthony LC cd6e0ef9e1 🔖(release) patch release 3.4.2
Changed:
- ️(docker) Optimize Dockerfile to use apk with --no-cache

Fixed:
- 🐛(backend) improve prompt to not use code blocks delimiter
2025-07-18 12:30:01 +02:00
AntoLC 02acc7233f 🌐(i18n) update translated strings
Update translated files with new translations
2025-07-18 12:30:01 +02:00
Anthony LC 1c71e830a2 🐛(backend) improve prompt to not use code blocks delimiter
The AI answer was activating the code block feature
in the editor, which was not desired.
The prompt for AI actions has been updated to
instruct the AI to return content directly
without wrapping it in code blocks or markdown
delimiters.
2025-07-18 12:03:48 +02:00
Peter Dave Hello ac0c16a44a ️(docker) Optimize Dockerfile to use apk with --no-cache
Using the `apk` commands with the `--no-cache` parameter for package
installation and upgrade will prevent the package index from being
cached and reduce the built image size.
2025-07-18 10:08:13 +02:00
virgile-dev ca09f9a158 📝(contributing) update contributing policy (#1192)
## Purpose
Update the issue template to help contributors

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`
- [ ] I have added a changelog entry under `## [Unreleased]` section (if
noticeable change)
- [ ] I have added corresponding tests for new features or bug fixes (if
applicable)

---------

Signed-off-by: virgile-dev <virgile.deville@beta.gouv.fr>
2025-07-17 09:58:31 +00:00
Anthony LC d12b608db9 🔖(patch) release 3.4.1
Fixed:
- 🌐(frontend) keep simple tag during export
- 🐛(back) manage can-edit endpoint without created room in the ws
- 🐛(frontend) fix action buttons not clickable
- 🐛(frontend) fix crash share modal on grid options
2025-07-15 16:14:43 +02:00
Anthony LC 08a0eb59c8 🐛(frontend) fix crash share modal on grid options
The share modal in the DocsGridItem component
was crashing when opened due to a provider not
initialized.
2025-07-15 11:36:44 +02:00
renovate[bot] 0afc50fb93 ⬆️(dependencies) update js dependencies 2025-07-15 03:15:51 +00:00
renovate[bot] c48a4309c1 ⬆️(dependencies) update python dependencies 2025-07-11 06:14:43 +00:00
Anthony LC a212417fb8 🐛(frontend) fix action buttons not clickable (#1162)
If the title was too long, or the children deepness too deep, the action
buttons in the doc tree were not clickable.
This commit fixes the issue by ensuring that the action buttons are
always clickable, regardless of the title length or children depth.
2025-07-11 08:13:01 +02:00
Manuel Raynaud 500d4ea5ac 🐛(back) manage can-edit endpoint without created room in the ws (#1152)
In a scenario where the first user is editing a docs without websocket
and nobody has reached the websocket server first, the y-provider
service will return a 404 and we don't handle this case in the can-edit
endpoint leading to a server error.
2025-07-10 12:24:38 +00:00
Anthony LC 8a057b9c39 🌐(i18n) update translated strings
Update translated files with new translations
2025-07-10 12:48:52 +02:00
Anthony LC 6a12ac560e 🌐(frontend) keep simple tag during export
When we export translations, we want to keep the
simple tags like `<strong>` instead of converting
it to `<1>` and `</1>`.
2025-07-10 12:38:28 +02:00
Anthony LC 2e6cb109ef 🔖(minor) release 3.4.0
Added:
- (frontend) multi-pages
- (frontend) Duplicate a doc
- Ask for access
- (frontend) add customization for translations
- (backend) add ancestors links definitions to document abilities
- (backend) include ancestors accesses on document accesses list view
- (backend) add ancestors links reach and role to document API
- 📝(project) add troubleshoot doc
- 📝(project) add system-requirement doc
- 🔧(front) configure x-frame-options to DENY in nginx conf
- (backend) allow to disable checking unsafe mimetype on attachment upload
- (doc) add documentation to install with compose
-  Give priority to users connected to collaboration server
  (aka no websocket feature)

Changed:
- ♻️(backend) stop requiring owner for non-root documents
- ♻️(backend) simplify roles by ranking them and return only the max role
- 📌(yjs) stop pinning node to minor version on yjs docker image
- 🧑‍💻(docker) add .next to .dockerignore
- 🧑‍💻(docker) handle frontend development images with docker compose
- 🧑‍💻(docker) add y-provider config to development environment
- ️(frontend) optimize document fetch error handling

Fixed:
- 🐛(backend) fix link definition select options linked to ancestors
- 🐛(frontend) table of content disappearing
- 🐛(frontend) fix multiple EmojiPicker
- 🐛(frontend) fix meta title
- 🔧(git) set LF line endings for all text files
- 📝(docs) minor fixes to docs/env.md
- support `_FILE` environment variables for secrets

Removed:
- 🔥(frontend) remove Beta from logo
2025-07-09 17:26:02 +02:00
Manuel Raynaud 70635136cb 🐛(back) duplicating a child should not create accesses
Children does not have accesses created for now, they inherit from their
parent for now. We have to ignore access creation while owrk on the
children accesses has not been made.
2025-07-09 17:26:02 +02:00
Anthony LC 52a8dd0b5c 🩹(frontend) refresh tree after duplicate
After duplicating a document, the tree is now
refreshed to reflect the new structure.
This ensures that the user sees the updated
document tree immediately after the duplication
action.
2025-07-09 17:26:02 +02:00
Anthony LC 8a3dfe0252 🛂(frontend) blocked edition if multiple ancestors
With child documents we need to check the parent
documents to know if the parent doc are collaborative
or not.
2025-07-09 17:26:02 +02:00
Anthony LC 1110ec92d5 (backend) fix test access create
Importing the french translation broke a test
because the subject was not in english anymore.
We change the admin user language to english
to keep the subject in english.
2025-07-09 17:26:02 +02:00
284 changed files with 24773 additions and 20050 deletions
-6
View File
@@ -1,6 +0,0 @@
<!---
Thanks for filing an issue 😄 ! Before you submit, please read the following:
Check the other issue templates if you are trying to submit a bug report, feature request, or question
Search open/closed issues before submitting since someone might have asked the same thing before!
-->
+4
View File
@@ -6,6 +6,10 @@ labels: ["bug", "triage"]
## Bug Report
**Before you file your issue**
- Check the other [issues](https://github.com/suitenumerique/docs/issues) before filing your own
- If your report is related to the ([BlockNote](https://github.com/TypeCellOS/BlockNote)) text editor, [file it on their repo](https://github.com/TypeCellOS/BlockNote/issues). If you're not sure whether your issue is with BlockNote or Docs, file it on our repo: if we support it, we'll backport it upstream ourselves 😊, otherwise we'll ask you to do so.
**Problematic behavior**
A clear and concise description of the behavior.
+2 -1
View File
@@ -23,9 +23,10 @@ jobs:
uses: actions/checkout@v4
# Backend i18n
- name: Install Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: "3.13.3"
cache: "pip"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
+54 -3
View File
@@ -80,7 +80,7 @@ jobs:
fail-on-cache-miss: true
- name: Set e2e env variables
run: cat env.d/development/common.e2e.dist >> env.d/development/common.dist
run: cat env.d/development/common.e2e >> env.d/development/common.local
- name: Install Playwright Browsers
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright chromium
@@ -101,7 +101,7 @@ jobs:
test-e2e-other-browser:
runs-on: ubuntu-latest
needs: test-e2e-chromium
timeout-minutes: 20
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -119,7 +119,7 @@ jobs:
fail-on-cache-miss: true
- name: Set e2e env variables
run: cat env.d/development/common.e2e.dist >> env.d/development/common.dist
run: cat env.d/development/common.e2e >> env.d/development/common.local
- name: Install Playwright Browsers
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright firefox webkit chromium
@@ -136,3 +136,54 @@ jobs:
name: playwright-other-report
path: src/frontend/apps/e2e/report/
retention-days: 7
bundle-size-check:
runs-on: ubuntu-latest
needs: install-dependencies
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Detect relevant changes
id: changes
uses: dorny/paths-filter@v2
with:
filters: |
lock:
- 'src/frontend/**/yarn.lock'
app:
- 'src/frontend/apps/impress/**'
- name: Restore the frontend cache
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
fail-on-cache-miss: true
- name: Setup Node.js
if: steps.changes.outputs.lock == 'true' || steps.changes.outputs.app == 'true'
uses: actions/setup-node@v4
with:
node-version: "22.x"
- name: Check bundle size changes
if: steps.changes.outputs.lock == 'true' || steps.changes.outputs.app == 'true'
uses: preactjs/compressed-size-action@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
build-script: "app:build"
pattern: "apps/impress/out/**/*.{css,js,html}"
exclude: "{**/*.map,**/node_modules/**}"
minimum-change-threshold: 500
compression: "gzip"
cwd: "./src/frontend"
show-total: true
strip-hash: "[-_.][a-f0-9]{8,}(?=\\.(?:js|css|html)$)"
omit-unchanged: true
install-script: "yarn install --frozen-lockfile"
+8 -2
View File
@@ -25,14 +25,18 @@ jobs:
- name: show
run: git log
- name: Enforce absence of print statements in code
if: always()
run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
- name: Check absence of fixup commits
if: always()
run: |
! git log | grep 'fixup!'
- name: Install gitlint
if: always()
run: pip install --user requests gitlint
- name: Lint commit messages added to main
if: always()
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
check-changelog:
@@ -89,9 +93,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: "3.13.3"
cache: "pip"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
@@ -184,9 +189,10 @@ jobs:
mc version enable impress/impress-media-storage"
- name: Install Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: "3.13.3"
cache: "pip"
- name: Install development dependencies
run: pip install --user .[dev]
+1 -2
View File
@@ -40,8 +40,7 @@ venv/
ENV/
env.bak/
venv.bak/
env.d/development/*
!env.d/development/*.dist
env.d/development/*.local
env.d/terraform
# npm
+136 -53
View File
@@ -8,22 +8,108 @@ and this project adheres to
## [Unreleased]
## [3.6.0] - 2025-09-04
### Added
- 👷(CI) add bundle size check job #1268
- ✨(frontend) use title first emoji as doc icon in tree #1289
### Changed
- ♻️(docs-app) Switch from Jest tests to Vitest #1269
- ♿(frontend) improve accessibility:
- 🌐(frontend) set html lang attribute dynamically #1248
- ♿(frontend) inject language attribute to pdf export #1235
- ♿(frontend) improve accessibility of search modal #1275
- ♿(frontend) add correct attributes to icons #1255
- 🎨(frontend) improve nav structure #1262
- ♿️(frontend) keyboard interaction with menu #1244
- ♿(frontend) improve header accessibility #1270
- ♿(frontend) improve accessibility for decorative images in editor #1282
- ♻️(backend) fallback to email identifier when no name #1298
- 🐛(backend) allow ASCII characters in user sub field #1295
- ⚡️(frontend) improve fallback width calculation #1333
### Fixed
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1263
- 🐛(minio) fix user permission error with Minio and Windows #1263
- 🐛(frontend) fix export when quote block and inline code #1319
- 🐛(frontend) fix base64 font #1324
- 🐛(backend) allow creator to delete subpages #1297
- 🐛(frontend) fix dnd conflict with tree and Blocknote #1328
- 🐛(frontend) fix display bug on homepage #1332
- 🐛link role update #1287
## [3.5.0] - 2025-07-31
### Added
- ✨(helm) Service Account support for K8s Resources in Helm Charts #780
- ✨(backend) allow masking documents from the list view #1172
- ✨(frontend) subdocs can manage link reach #1190
- ✨(frontend) add duplicate action to doc tree #1175
- ✨(frontend) Interlinking doc #904
- ✨(frontend) add multi columns support for editor #1219
### Changed
- ♻️(frontend) search on all docs if no children #1184
- ♻️(frontend) redirect to doc after duplicate #1175
- 🔧(project) change env.d system by using local files #1200
- ⚡️(frontend) improve tree stability #1207
- ⚡️(frontend) improve accessibility #1232
- 🛂(frontend) block drag n drop when not desktop #1239
### Fixed
- 🐛(service-worker) Fix useOffline Maximum update depth exceeded #1196
- 🐛(frontend) fix empty left panel after deleting root doc #1197
- 🐛(helm) charts generate invalid YAML for collaboration API / WS #890
- 🐛(frontend) 401 redirection overridden #1214
- 🐛(frontend) include root parent in search #1243
## [3.4.2] - 2025-07-18
### Changed
- ⚡️(docker) Optimize Dockerfile to use apk with --no-cache #743
### Fixed
- 🐛(backend) improve prompt to not use code blocks delimiter #1188
## [3.4.1] - 2025-07-15
### Fixed
- 🌐(frontend) keep simple tag during export #1154
- 🐛(back) manage can-edit endpoint without created room
in the ws #1152
- 🐛(frontend) fix action buttons not clickable #1162
- 🐛(frontend) fix crash share modal on grid options #1174
- 🐛(frontend) fix unfold subdocs not clickable at the bottom #1179
## [3.4.0] - 2025-07-09
### Added
- ✨(frontend) multi-pages #701
- ✨(backend) add ancestors links definitions to document abilities #846
- ✨(backend) include ancestors accesses on document accesses list view # 846
- ✨(backend) add ancestors links reach and role to document API #846
- ✨(frontend) add customization for translations #857
- ✨(frontend) Duplicate a doc #1078
- ✨Ask for access #1081
- ✨(frontend) add customization for translations #857
- ✨(backend) add ancestors links definitions to document abilities #846
- ✨(backend) include ancestors accesses on document accesses list view #846
- ✨(backend) add ancestors links reach and role to document API #846
- 📝(project) add troubleshoot doc #1066
- 📝(project) add system-requirement doc #1066
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
- ✨(backend) allow to disable checking unsafe mimetype on attachment upload
- ✨Ask for access #1081
- 🔧(frontend) configure x-frame-options to DENY in nginx conf #1084
- ✨(backend) allow to disable checking unsafe mimetype on
attachment upload #1099
- ✨(doc) add documentation to install with compose #855
- ✨ Give priority to users connected to collaboration server
(aka no websocket feature) #1093
- ✨ Give priority to users connected to collaboration server
(aka no websocket feature) #1093
### Changed
@@ -43,14 +129,12 @@ and this project adheres to
- 🐛(frontend) fix meta title #1017
- 🔧(git) set LF line endings for all text files #1032
- 📝(docs) minor fixes to docs/env.md
-(backend) support `_FILE` environment variables for secrets #912
- ✨(frontend) support `_FILE` environment variables for secrets #912
- ✨support `_FILE` environment variables for secrets #912
### Removed
- 🔥(frontend) remove Beta from logo #1095
## [3.3.0] - 2025-05-06
### Added
@@ -76,13 +160,13 @@ and this project adheres to
- ⬆️(docker) upgrade node images to alpine 3.21 #973
### Fixed
- 🐛(y-provider) increase JSON size limits for transcription conversion #989
### Removed
- 🔥(back) remove footer endpoint #948
## [3.2.1] - 2025-05-06
## Fixed
@@ -90,7 +174,6 @@ and this project adheres to
- 🐛(frontend) fix list copy paste #943
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
## [3.2.0] - 2025-05-05
## Added
@@ -101,7 +184,7 @@ and this project adheres to
- ✨(settings) Allow configuring PKCE for the SSO #886
- 🌐(i18n) activate chinese and spanish languages #884
- 🔧(backend) allow overwriting the data directory #893
- (backend) add `django-lasuite` dependency #839
- (backend) add `django-lasuite` dependency #839
- ✨(frontend) advanced table features #908
## Changed
@@ -118,7 +201,6 @@ and this project adheres to
- 🐛(backend) race condition create doc #633
- 🐛(frontend) fix breaklines in custom blocks #908
## [3.1.0] - 2025-04-07
## Added
@@ -153,7 +235,6 @@ and this project adheres to
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
- 🔒️(back) restrict access to document accesses #801
## [2.6.0] - 2025-03-21
## Added
@@ -172,7 +253,6 @@ and this project adheres to
- 🔒️(back) throttle user list endpoint #636
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
## [2.5.0] - 2025-03-18
## Added
@@ -195,15 +275,14 @@ and this project adheres to
## Fixed
- 🐛(frontend) SVG export #706
- 🐛(frontend) remove scroll listener table content #688
- 🐛(frontend) remove scroll listener table content #688
- 🔒️(back) restrict access to favorite_list endpoint #690
- 🐛(backend) refactor to fix filtering on children
and descendants views #695
- 🐛(backend) refactor to fix filtering on children
and descendants views #695
- 🐛(action) fix notify-argocd workflow #713
- 🚨(helm) fix helmfile lint #736
- 🚚(frontend) redirect to 401 page when 401 error #759
## [2.4.0] - 2025-03-06
## Added
@@ -218,7 +297,6 @@ and this project adheres to
- 🐛(frontend) fix collaboration error #684
## [2.3.0] - 2025-03-03
## Added
@@ -634,33 +712,38 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.3.0...main
[v3.3.0]: https://github.com/numerique-gouv/impress/releases/v3.3.0
[v3.2.1]: https://github.com/numerique-gouv/impress/releases/v3.2.1
[v3.2.0]: https://github.com/numerique-gouv/impress/releases/v3.2.0
[v3.1.0]: https://github.com/numerique-gouv/impress/releases/v3.1.0
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
[v2.6.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0
[v2.4.0]: https://github.com/numerique-gouv/impress/releases/v2.4.0
[v2.3.0]: https://github.com/numerique-gouv/impress/releases/v2.3.0
[v2.2.0]: https://github.com/numerique-gouv/impress/releases/v2.2.0
[v2.1.0]: https://github.com/numerique-gouv/impress/releases/v2.1.0
[v2.0.1]: https://github.com/numerique-gouv/impress/releases/v2.0.1
[v2.0.0]: https://github.com/numerique-gouv/impress/releases/v2.0.0
[v1.10.0]: https://github.com/numerique-gouv/impress/releases/v1.10.0
[v1.9.0]: https://github.com/numerique-gouv/impress/releases/v1.9.0
[v1.8.2]: https://github.com/numerique-gouv/impress/releases/v1.8.2
[v1.8.1]: https://github.com/numerique-gouv/impress/releases/v1.8.1
[v1.8.0]: https://github.com/numerique-gouv/impress/releases/v1.8.0
[v1.7.0]: https://github.com/numerique-gouv/impress/releases/v1.7.0
[v1.6.0]: https://github.com/numerique-gouv/impress/releases/v1.6.0
[1.5.1]: https://github.com/numerique-gouv/impress/releases/v1.5.1
[1.5.0]: https://github.com/numerique-gouv/impress/releases/v1.5.0
[1.4.0]: https://github.com/numerique-gouv/impress/releases/v1.4.0
[1.3.0]: https://github.com/numerique-gouv/impress/releases/v1.3.0
[1.2.1]: https://github.com/numerique-gouv/impress/releases/v1.2.1
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.6.0...main
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
[v3.4.2]: https://github.com/suitenumerique/docs/releases/v3.4.2
[v3.4.1]: https://github.com/suitenumerique/docs/releases/v3.4.1
[v3.4.0]: https://github.com/suitenumerique/docs/releases/v3.4.0
[v3.3.0]: https://github.com/suitenumerique/docs/releases/v3.3.0
[v3.2.1]: https://github.com/suitenumerique/docs/releases/v3.2.1
[v3.2.0]: https://github.com/suitenumerique/docs/releases/v3.2.0
[v3.1.0]: https://github.com/suitenumerique/docs/releases/v3.1.0
[v3.0.0]: https://github.com/suitenumerique/docs/releases/v3.0.0
[v2.6.0]: https://github.com/suitenumerique/docs/releases/v2.6.0
[v2.5.0]: https://github.com/suitenumerique/docs/releases/v2.5.0
[v2.4.0]: https://github.com/suitenumerique/docs/releases/v2.4.0
[v2.3.0]: https://github.com/suitenumerique/docs/releases/v2.3.0
[v2.2.0]: https://github.com/suitenumerique/docs/releases/v2.2.0
[v2.1.0]: https://github.com/suitenumerique/docs/releases/v2.1.0
[v2.0.1]: https://github.com/suitenumerique/docs/releases/v2.0.1
[v2.0.0]: https://github.com/suitenumerique/docs/releases/v2.0.0
[v1.10.0]: https://github.com/suitenumerique/docs/releases/v1.10.0
[v1.9.0]: https://github.com/suitenumerique/docs/releases/v1.9.0
[v1.8.2]: https://github.com/suitenumerique/docs/releases/v1.8.2
[v1.8.1]: https://github.com/suitenumerique/docs/releases/v1.8.1
[v1.8.0]: https://github.com/suitenumerique/docs/releases/v1.8.0
[v1.7.0]: https://github.com/suitenumerique/docs/releases/v1.7.0
[v1.6.0]: https://github.com/suitenumerique/docs/releases/v1.6.0
[1.5.1]: https://github.com/suitenumerique/docs/releases/v1.5.1
[1.5.0]: https://github.com/suitenumerique/docs/releases/v1.5.0
[1.4.0]: https://github.com/suitenumerique/docs/releases/v1.4.0
[1.3.0]: https://github.com/suitenumerique/docs/releases/v1.3.0
[1.2.1]: https://github.com/suitenumerique/docs/releases/v1.2.1
[1.2.0]: https://github.com/suitenumerique/docs/releases/v1.2.0
[1.1.0]: https://github.com/suitenumerique/docs/releases/v1.1.0
[1.0.0]: https://github.com/suitenumerique/docs/releases/v1.0.0
[0.1.0]: https://github.com/suitenumerique/docs/releases/v0.1.0
+4 -5
View File
@@ -7,8 +7,7 @@ FROM python:3.13.3-alpine AS base
RUN python -m pip install --upgrade pip setuptools
# Upgrade system packages to install security updates
RUN apk update && \
apk upgrade
RUN apk update && apk upgrade --no-cache
# ---- Back-end builder image ----
FROM base AS back-builder
@@ -45,7 +44,7 @@ FROM base AS link-collector
ARG IMPRESS_STATIC_ROOT=/data/static
# Install pango & rdfind
RUN apk add \
RUN apk add --no-cache \
pango \
rdfind
@@ -71,7 +70,7 @@ FROM base AS core
ENV PYTHONUNBUFFERED=1
# Install required system libs
RUN apk add \
RUN apk add --no-cache \
cairo \
file \
font-noto \
@@ -117,7 +116,7 @@ FROM core AS backend-development
USER root:root
# Install psql
RUN apk add postgresql-client
RUN apk add --no-cache postgresql-client
# Uninstall impress and re-install it in editable mode along with development
# dependencies
+86 -28
View File
@@ -35,9 +35,13 @@ DB_PORT = 5432
# -- Docker
# Get the current user ID to use for docker run and docker exec commands
DOCKER_UID = $(shell id -u)
DOCKER_GID = $(shell id -g)
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
ifeq ($(OS),Windows_NT)
DOCKER_USER := 0:0 # run containers as root on Windows
else
DOCKER_UID := $(shell id -u)
DOCKER_GID := $(shell id -g)
DOCKER_USER := $(DOCKER_UID):$(DOCKER_GID)
endif
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
COMPOSE_E2E = DOCKER_USER=$(DOCKER_USER) docker compose -f compose.yml -f compose-e2e.yml
COMPOSE_EXEC = $(COMPOSE) exec
@@ -48,7 +52,7 @@ COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
# -- Backend
MANAGE = $(COMPOSE_RUN_APP) python manage.py
MAIL_YARN = $(COMPOSE_RUN) -w /app/src/mail node yarn
MAIL_YARN = $(COMPOSE_RUN) -w //app/src/mail node yarn
# -- Frontend
PATH_FRONT = ./src/frontend
@@ -67,18 +71,18 @@ data/static:
# -- Project
create-env-files: ## Copy the dist env files to env files
create-env-files: \
env.d/development/common \
env.d/development/crowdin \
env.d/development/postgresql \
env.d/development/kc_postgresql
.PHONY: create-env-files
create-env-local-files: ## create env.local files in env.d/development
create-env-local-files:
@touch env.d/development/crowdin.local
@touch env.d/development/common.local
@touch env.d/development/postgresql.local
@touch env.d/development/kc_postgresql.local
.PHONY: create-env-local-files
pre-bootstrap: \
data/media \
data/static \
create-env-files
create-env-local-files
.PHONY: pre-bootstrap
post-bootstrap: \
@@ -89,13 +93,77 @@ post-bootstrap: \
mails-build
.PHONY: post-bootstrap
pre-beautiful-bootstrap: ## Display a welcome message before bootstrap
ifeq ($(OS),Windows_NT)
@echo ""
@echo "================================================================================"
@echo ""
@echo " Welcome to Docs - Collaborative Text Editing from La Suite!"
@echo ""
@echo " This will set up your development environment with:"
@echo " - Docker containers for all services"
@echo " - Database migrations and static files"
@echo " - Frontend dependencies and build"
@echo " - Environment configuration files"
@echo ""
@echo " Services will be available at:"
@echo " - Frontend: http://localhost:3000"
@echo " - API: http://localhost:8071"
@echo " - Admin: http://localhost:8071/admin"
@echo ""
@echo "================================================================================"
@echo ""
@echo "Starting bootstrap process..."
else
@echo "$(BOLD)"
@echo "╔══════════════════════════════════════════════════════════════════════════════╗"
@echo "║ ║"
@echo "║ 🚀 Welcome to Docs - Collaborative Text Editing from La Suite ! 🚀 ║"
@echo "║ ║"
@echo "║ This will set up your development environment with : ║"
@echo "║ • Docker containers for all services ║"
@echo "║ • Database migrations and static files ║"
@echo "║ • Frontend dependencies and build ║"
@echo "║ • Environment configuration files ║"
@echo "║ ║"
@echo "║ Services will be available at: ║"
@echo "║ • Frontend: http://localhost:3000 ║"
@echo "║ • API: http://localhost:8071 ║"
@echo "║ • Admin: http://localhost:8071/admin ║"
@echo "║ ║"
@echo "╚══════════════════════════════════════════════════════════════════════════════╝"
@echo "$(RESET)"
@echo "$(GREEN)Starting bootstrap process...$(RESET)"
endif
@echo ""
.PHONY: pre-beautiful-bootstrap
bootstrap: ## Prepare Docker developmentimages for the project
post-beautiful-bootstrap: ## Display a success message after bootstrap
@echo ""
ifeq ($(OS),Windows_NT)
@echo "Bootstrap completed successfully!"
@echo ""
@echo "Next steps:"
@echo " - Visit http://localhost:3000 to access the application"
@echo " - Run 'make help' to see all available commands"
else
@echo "$(GREEN)🎉 Bootstrap completed successfully!$(RESET)"
@echo ""
@echo "$(BOLD)Next steps:$(RESET)"
@echo " • Visit http://localhost:3000 to access the application"
@echo " • Run 'make help' to see all available commands"
endif
@echo ""
.PHONY: post-beautiful-bootstrap
bootstrap: ## Prepare the project for local development
bootstrap: \
pre-beautiful-bootstrap \
pre-bootstrap \
build \
post-bootstrap \
run
run \
post-beautiful-bootstrap
.PHONY: bootstrap
bootstrap-e2e: ## Prepare Docker production images to be used for e2e tests
@@ -258,20 +326,6 @@ resetdb: ## flush database and create a superuser "admin"
@${MAKE} superuser
.PHONY: resetdb
env.d/development/common:
cp -n env.d/development/common.dist env.d/development/common
env.d/development/postgresql:
cp -n env.d/development/postgresql.dist env.d/development/postgresql
env.d/development/kc_postgresql:
cp -n env.d/development/kc_postgresql.dist env.d/development/kc_postgresql
# -- Internationalization
env.d/development/crowdin:
cp -n env.d/development/crowdin.dist env.d/development/crowdin
crowdin-download: ## Download translated message from crowdin
@$(COMPOSE_RUN_CROWDIN) download -c crowdin/config.yml
.PHONY: crowdin-download
@@ -352,6 +406,10 @@ run-frontend-development: ## Run the frontend in development mode
cd $(PATH_FRONT_IMPRESS) && yarn dev
.PHONY: run-frontend-development
frontend-test: ## Run the frontend tests
cd $(PATH_FRONT_IMPRESS) && yarn test
.PHONY: frontend-test
frontend-i18n-extract: ## Extract the frontend translation inside a json to be used for crowdin
cd $(PATH_FRONT) && yarn i18n:extract
.PHONY: frontend-i18n-extract
+21 -4
View File
@@ -49,13 +49,24 @@ Docs is a collaborative text editor designed to address common challenges in kno
* 📚 Turn your team's collaborative work into organized knowledge with Subpages.
### Self-host
🚀 Docs is easy to install on your own servers
Available methods: Helm chart, Nix package
#### 🚀 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.
In the works: Docker Compose, YunoHost
#### 🌍 Known instances
We hope to see many more, here is an incomplete list of public Docs instances (urls listed in alphabetical order). Feel free to make a PR to add ones that are not listed below🙏
| | | |
| --- | --- | ------- |
| Url | Org | Public |
| 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 | 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 | ZenDiS | Demo instance of OpenDesk. Request access to get credentials |
| notes.liiib.re | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
| docs.federated.nexus | federated.nexus | Public instance, but you have to [sign up for a Federated Nexus account](https://federated.nexus/register/). |
⚠️ For some advanced features (ex: Export as PDF) Docs relies on XL packages from BlockNote. These are licenced under AGPL-3.0 and are not MIT compatible. You can perfectly use Docs without these packages by setting the environment variable `PUBLISH_AS_MIT` to true. That way you'll build an image of the application without the features that are not MIT compatible. Read the [environment variables documentation](/docs/env.md) for more information.
#### ⚠️ 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.
## Getting started 🔧
@@ -130,6 +141,12 @@ To start all the services, except the frontend container, you can use the follow
$ make run-backend
```
To execute frontend tests & linting only
```shellscript
$ make frontend-test
$ make frontend-lint
```
**Adding content**
You can create a basic demo site by running this command:
+4
View File
@@ -38,6 +38,10 @@ function _set_user() {
# options: docker compose command options
# ARGS : docker compose command arguments
function _docker_compose() {
# Set DOCKER_USER for Windows compatibility with MinIO
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || -n "${WSL_DISTRO_NAME:-}" ]]; then
export DOCKER_USER="0:0"
fi
echo "🐳(compose) file: '${COMPOSE_FILE}'"
docker compose \
+1
View File
@@ -24,5 +24,6 @@ services:
restart: unless-stopped
env_file:
- env.d/development/common
- env.d/development/common.local
ports:
- "4444:4444"
+8
View File
@@ -10,6 +10,7 @@ services:
retries: 300
env_file:
- env.d/development/postgresql
- env.d/development/postgresql.local
ports:
- "15432:5432"
@@ -66,7 +67,9 @@ services:
- DJANGO_CONFIGURATION=Development
env_file:
- env.d/development/common
- env.d/development/common.local
- env.d/development/postgresql
- env.d/development/postgresql.local
ports:
- "8071:8000"
volumes:
@@ -91,7 +94,9 @@ services:
- DJANGO_CONFIGURATION=Development
env_file:
- env.d/development/common
- env.d/development/common.local
- env.d/development/postgresql
- env.d/development/postgresql.local
volumes:
- ./src/backend:/app
- ./data/static:/data/static
@@ -135,6 +140,7 @@ services:
- ".:/app"
env_file:
- env.d/development/crowdin
- env.d/development/crowdin.local
user: "${DOCKER_USER:-1000}"
working_dir: /app
@@ -156,6 +162,7 @@ services:
restart: unless-stopped
env_file:
- env.d/development/common
- env.d/development/common.local
ports:
- "4444:4444"
volumes:
@@ -174,6 +181,7 @@ services:
- "5433:5432"
env_file:
- env.d/development/kc_postgresql
- env.d/development/kc_postgresql.local
keycloak:
image: quay.io/keycloak/keycloak:20.0.1
@@ -11,6 +11,9 @@ server {
server_name localhost;
charset utf-8;
# increase max upload size
client_max_body_size 10m;
# Disables server version feedback on pages and in headers
server_tokens off;
@@ -68,7 +71,7 @@ server {
proxy_set_header Host $host;
}
location /collaboration/api/ {
location /collaboration/api/ {
# Collaboration server
proxy_pass http://${YPROVIDER_HOST}:4444;
proxy_set_header Host $host;
@@ -95,7 +98,7 @@ server {
add_header Content-Security-Policy "default-src 'none'" always;
}
location /media-auth {
proxy_pass http://docs_backend/api/v1.0/documents/media-auth/;
proxy_set_header X-Forwarded-Proto https;
@@ -109,4 +112,4 @@ server {
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
}
}
}
+4 -3
View File
@@ -135,10 +135,11 @@ NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
| PUBLISH_AS_MIT | Removes packages whose licences are incompatible with the MIT licence (see below) | true |
Packages with licences incompatible with the MIT licence:
* `xl-docx-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE)
* `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](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE) or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.
⚠️ 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.
+15 -15
View File
@@ -7,12 +7,12 @@ services:
timeout: 2s
retries: 300
env_file:
- env.d/postgresql
- env.d/common
- env.d/postgresql
- env.d/common
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- ./data/databases/backend:/var/lib/postgresql/data/pgdata
- ./data/databases/backend:/var/lib/postgresql/data/pgdata
redis:
image: redis:8
@@ -22,12 +22,12 @@ services:
user: ${DOCKER_USER:-1000}
restart: always
environment:
- DJANGO_CONFIGURATION=Production
- DJANGO_CONFIGURATION=Production
env_file:
- env.d/common
- env.d/backend
- env.d/yprovider
- env.d/postgresql
- env.d/common
- env.d/backend
- env.d/yprovider
- env.d/postgresql
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 15s
@@ -45,24 +45,24 @@ services:
image: lasuite/impress-y-provider:latest
user: ${DOCKER_USER:-1000}
env_file:
- env.d/common
- env.d/yprovider
- env.d/common
- env.d/yprovider
frontend:
image: lasuite/impress-frontend:latest
user: "101"
entrypoint:
- /docker-entrypoint.sh
- /docker-entrypoint.sh
command: ["nginx", "-g", "daemon off;"]
env_file:
- env.d/common
- env.d/common
# Uncomment and set your values if using our nginx proxy example
#environment:
# - VIRTUAL_HOST=${DOCS_HOST} # used by nginx proxy
# - VIRTUAL_HOST=${DOCS_HOST} # used by nginx proxy
# - VIRTUAL_PORT=8083 # used by nginx proxy
# - LETSENCRYPT_HOST=${DOCS_HOST} # used by lets encrypt to generate TLS certificate
volumes:
- ./default.conf.template:/etc/nginx/templates/docs.conf.template
- ./default.conf.template:/etc/nginx/templates/docs.conf.template
depends_on:
backend:
condition: service_healthy
+3 -3
View File
@@ -9,9 +9,9 @@
```bash
mkdir keycloak
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
curl -o env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
curl -o env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
curl -o keycloak/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
curl -o keycloak/env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
curl -o keycloak/env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
```
### Step 2:. Update `env.d/` files
+7 -7
View File
@@ -7,23 +7,23 @@ services:
timeout: 2s
retries: 300
env_file:
- env.d/kc_postgresql
- env.d/kc_postgresql
volumes:
- ./data/keycloak:/var/lib/postgresql/data/pgdata
- ./data/keycloak:/var/lib/postgresql/data/pgdata
keycloak:
image: quay.io/keycloak/keycloak:26.1.3
command: ["start"]
env_file:
- env.d/kc_postgresql
- env.d/keycloak
- env.d/kc_postgresql
- env.d/keycloak
# Uncomment and set your values if using our nginx proxy example
# environment:
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
# - VIRTUAL_PORT=8080 # used by nginx proxy
# - LETSENCRYPT_HOST=id.yourdomain.tld # used by lets encrypt to generate TLS certificate
depends_on:
kc_postgresql::
kc_postgresql:
condition: service_healthy
restart: true
# Uncomment if using our nginx proxy example
@@ -33,4 +33,4 @@ services:
#
#networks:
# proxy-tier:
# external: true
# external: true
+1 -1
View File
@@ -9,7 +9,7 @@
```bash
mkdir minio
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
curl -o minio/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
```
### Step 2:. Update compose file with your own values
+5 -5
View File
@@ -2,8 +2,8 @@ services:
minio:
image: minio/minio
environment:
- MINIO_ROOT_USER=<set minio root username>
- MINIO_ROOT_PASSWORD=<set minio root password>
- MINIO_ROOT_USER=<set minio root username>
- MINIO_ROOT_PASSWORD=<set minio root password>
# Uncomment and set your values if using our nginx proxy example
# - VIRTUAL_HOST=storage.yourdomain.tld # used by nginx proxy
# - VIRTUAL_PORT=9000 # used by nginx proxy
@@ -16,12 +16,12 @@ services:
entrypoint: ""
command: minio server /data
volumes:
- ./data/minio:/data
- ./data/minio:/data
# Uncomment if using our nginx proxy example
# networks:
# - proxy-tier
# - proxy-tier
# Uncomment if using our nginx proxy example
#networks:
# proxy-tier:
# external: true
# external: true
+1 -1
View File
@@ -13,7 +13,7 @@ Acme-companion is a lightweight companion container for nginx-proxy. It handles
```bash
mkdir nginx-proxy
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
curl -o nginx-proxy/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
```
### Step 2: Edit `DEFAULT_EMAIL` in the compose file.
+12 -12
View File
@@ -3,28 +3,28 @@ services:
image: nginxproxy/nginx-proxy
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
- "80:80"
- "443:443"
volumes:
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
networks:
- proxy-tier
- proxy-tier
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
environment:
- DEFAULT_EMAIL=mail@yourdomain.tld
- DEFAULT_EMAIL=mail@yourdomain.tld
volumes_from:
- nginx-proxy
- nginx-proxy
volumes:
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
- certs:/etc/nginx/certs:rw
- acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy-tier
- proxy-tier
networks:
proxy-tier:
+1 -4
View File
@@ -27,7 +27,7 @@ backend:
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/session/end
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
@@ -46,9 +46,6 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: root
+32
View File
@@ -0,0 +1,32 @@
# Installation
If you want to install Docs you've come to the right place.
Here are a bunch of resources to help you install the project.
## Kubernetes
We (Docs maintainers) are only using the Kubernetes deployment method in production. We can only provide advanced support for this method.
Please follow the instructions laid out [here](/docs/installation/kubernetes.md).
## Docker Compose
We are aware that not everyone has Kubernetes Cluster laying around 😆.
We also provide [Docker images](https://hub.docker.com/u/lasuite?page=1&search=impress) that you can deploy using Compose.
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.
Here is the list of other methods in alphabetical order:
- Coop-Cloud: [code](https://git.coopcloud.tech/coop-cloud/lasuite-docs)
- Nix: [Packages](https://search.nixos.org/packages?channel=unstable&query=lasuite-docs), ⚠️ unstable
- Podman: [code][https://codeberg.org/philo/lasuite-docs-podman], ⚠️ experimental
- YunoHost: [code](https://github.com/YunoHost-Apps/lasuite-docs_ynh), [app store](https://apps.yunohost.org/app/lasuite-docs)
Feel free to make a PR to add ones that are not listed above 🙏
## Cloud providers
Some cloud providers are making it easy to deploy Docs on their infrastructure.
Here is the list in alphabetical order:
- Clever Cloud 🇫🇷 : [market place][https://www.clever-cloud.com/product/docs/], [technical doc](https://www.clever.cloud/developers/guides/docs/#deploy-docs)
Feel free to make a PR to add ones that are not listed above 🙏
+8 -2
View File
@@ -1,6 +1,6 @@
# Installation with docker compose
We provide a sample configuration for running Docs using Docker Compose. Please note that this configuration is experimental, and the official way to deploy Docs in production is to use [k8s](../installation/k8s.md)
We provide a sample configuration for running Docs using Docker Compose. Please note that this configuration is experimental, and the official way to deploy Docs in production is to use [k8s](../installation/kubernetes.md)
## Requirements
@@ -31,11 +31,17 @@ For older versions of Docker Engine that do not include Docker Compose:
```bash
mkdir -p docs/env.d
cd docs
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/compose.yaml
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/common
curl -o env.d/backend https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/backend
curl -o env.d/yprovider https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/yprovider
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
curl -o env.d/postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
```
If you are using the sample nginx-proxy configuration:
```bash
curl -o default.conf.template https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docker/files/production/etc/nginx/conf.d/default.conf.template
```
## Step 2: Configuration
+1 -4
View File
@@ -124,7 +124,7 @@ OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/session/end
OIDC_OP_LOGOUT_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/impress/protocol/openid-connect/logout
OIDC_RP_CLIENT_ID: impress
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
@@ -168,9 +168,6 @@ DB_NAME: impress
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
### Find s3 bucket connection values
-49
View File
@@ -83,55 +83,6 @@ If you already have CRLF line endings in your local repository, the **best appro
git commit -m "✏️(project) Fix line endings to LF"
```
## Minio Permission Issues on Windows
### Problem Description
On Windows, you may encounter permission-related errors when running Minio in development mode with Docker Compose. This typically happens because:
- **Windows file permissions** don't map well to Unix-style user IDs used in Docker containers
- **Docker Desktop** may have issues with user mapping when using the `DOCKER_USER` environment variable
- **Minio container** fails to start or access volumes due to permission conflicts
### Common Symptoms
- Minio container fails to start with permission denied errors
- Error messages related to file system permissions in Minio logs
- Unable to create or access buckets in the development environment
- Docker Compose showing Minio service as unhealthy or exited
### Solution for Windows Users
If you encounter Minio permission issues on Windows, you can temporarily disable user mapping for the Minio service:
1. **Open the `compose.yml` file**
2. **Comment out the user directive** in the `minio` service section:
```yaml
minio:
# user: ${DOCKER_USER:-1000} # Comment this line on Windows if permission issues occur
image: minio/minio
environment:
- MINIO_ROOT_USER=impress
- MINIO_ROOT_PASSWORD=password
# ... rest of the configuration
```
3. **Restart the services**:
```bash
make run
```
### Why This Works
- Commenting out the `user` directive allows the Minio container to run with its default user
- This bypasses Windows-specific permission mapping issues
- The container will have the necessary permissions to access and manage the mounted volumes
### Note
This is a **development-only workaround**. In production environments, proper user mapping and security considerations should be maintained according to your deployment requirements.
## Frontend File Watching Issues on Windows
### Problem Description
+2 -2
View File
@@ -43,8 +43,8 @@ OIDC_RP_CLIENT_ID=<client_id>
OIDC_RP_CLIENT_SECRET=<client secret>
OIDC_RP_SIGN_ALGO=RS256
OIDC_RP_SCOPES="openid email"
#USER_OIDC_FIELD_TO_SHORTNAME
#USER_OIDC_FIELDS_TO_FULLNAME
#OIDC_USERINFO_SHORTNAME_FIELD
#OIDC_USERINFO_FULLNAME_FIELDS
LOGIN_REDIRECT_URL=https://${DOCS_HOST}
LOGIN_REDIRECT_URL_FAILURE=https://${DOCS_HOST}
+1 -1
View File
@@ -1,4 +1,4 @@
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api/
Y_PROVIDER_API_KEY=<generate a random key>
COLLABORATION_SERVER_SECRET=<generate a random key>
COLLABORATION_SERVER_ORIGIN=https://${DOCS_HOST}
+4
View File
@@ -2,6 +2,10 @@
"extends": ["github>numerique-gouv/renovate-configuration"],
"dependencyDashboard": true,
"labels": ["dependencies", "noChangeLog", "automated"],
"schedule": ["before 7am on monday"],
"prCreation": "not-pending",
"rebaseWhen": "conflicted",
"updateNotScheduled": false,
"packageRules": [
{
"enabled": false,
+22
View File
@@ -60,6 +60,9 @@ class ListDocumentFilter(DocumentFilter):
is_creator_me = django_filters.BooleanFilter(
method="filter_is_creator_me", label=_("Creator is me")
)
is_masked = django_filters.BooleanFilter(
method="filter_is_masked", label=_("Masked")
)
is_favorite = django_filters.BooleanFilter(
method="filter_is_favorite", label=_("Favorite")
)
@@ -106,3 +109,22 @@ class ListDocumentFilter(DocumentFilter):
return queryset
return queryset.filter(is_favorite=bool(value))
# pylint: disable=unused-argument
def filter_is_masked(self, queryset, name, value):
"""
Filter documents based on whether they are masked by the current user.
Example:
- /api/v1.0/documents/?is_masked=true
→ Filters documents marked as masked by the logged-in user
- /api/v1.0/documents/?is_masked=false
→ Filters documents not marked as masked by the logged-in user
"""
user = self.request.user
if not user.is_authenticated:
return queryset
queryset_method = queryset.filter if bool(value) else queryset.exclude
return queryset_method(link_traces__user=user, link_traces__is_masked=True)
+22 -2
View File
@@ -7,12 +7,13 @@ from base64 import b64decode
from django.conf import settings
from django.db.models import Q
from django.utils.functional import lazy
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
import magic
from rest_framework import serializers
from core import choices, enums, models, utils
from core import choices, enums, models, utils, validators
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
ConversionError,
@@ -32,11 +33,30 @@ class UserSerializer(serializers.ModelSerializer):
class UserLightSerializer(UserSerializer):
"""Serialize users with limited fields."""
full_name = serializers.SerializerMethodField(read_only=True)
short_name = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.User
fields = ["full_name", "short_name"]
read_only_fields = ["full_name", "short_name"]
def get_full_name(self, instance):
"""Return the full name of the user."""
if not instance.full_name:
email = instance.email.split("@")[0]
return slugify(email)
return instance.full_name
def get_short_name(self, instance):
"""Return the short name of the user."""
if not instance.short_name:
email = instance.email.split("@")[0]
return slugify(email)
return instance.short_name
class TemplateAccessSerializer(serializers.ModelSerializer):
"""Serialize template accesses."""
@@ -402,7 +422,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
content = serializers.CharField(required=True)
# User
sub = serializers.CharField(
required=True, validators=[models.User.sub_validator], max_length=255
required=True, validators=[validators.sub_validator], max_length=255
)
email = serializers.EmailField(required=True)
language = serializers.ChoiceField(
+104 -41
View File
@@ -13,6 +13,7 @@ from django.contrib.postgres.search import TrigramSimilarity
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.core.validators import URLValidator
from django.db import connection, transaction
from django.db import models as db
from django.db.models.expressions import RawSQL
@@ -359,7 +360,7 @@ class DocumentViewSet(
permission_classes = [
permissions.DocumentPermission,
]
queryset = models.Document.objects.all()
queryset = models.Document.objects.select_related("creator").all()
serializer_class = serializers.DocumentSerializer
ai_translate_serializer_class = serializers.AITranslateSerializer
children_serializer_class = serializers.ListDocumentSerializer
@@ -455,9 +456,8 @@ class DocumentViewSet(
# Annotate favorite status and filter if applicable as late as possible
queryset = queryset.annotate_is_favorite(user)
queryset = filterset.filters["is_favorite"].filter(
queryset, filter_data["is_favorite"]
)
for field in ["is_favorite", "is_masked"]:
queryset = filterset.filters[field].filter(queryset, filter_data[field])
# Apply ordering only now that everything is filtered and annotated
queryset = filters.OrderingFilter().filter_queryset(
@@ -787,7 +787,11 @@ class DocumentViewSet(
)
# GET: List children
queryset = document.get_children().filter(ancestors_deleted_at__isnull=True)
queryset = (
document.get_children()
.select_related("creator")
.filter(ancestors_deleted_at__isnull=True)
)
queryset = self.filter_queryset(queryset)
filterset = DocumentFilter(request.GET, queryset=queryset)
@@ -841,19 +845,27 @@ class DocumentViewSet(
user = self.request.user
try:
current_document = self.queryset.only("depth", "path").get(pk=pk)
current_document = (
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
)
except models.Document.DoesNotExist as excpt:
raise drf.exceptions.NotFound() from excpt
ancestors = (
(current_document.get_ancestors() | self.queryset.filter(pk=pk))
(
current_document.get_ancestors()
| self.queryset.select_related(None).filter(pk=pk)
)
.filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
# Get the highest readable ancestor
highest_readable = (
ancestors.readable_per_se(request.user).only("depth", "path").first()
ancestors.select_related(None)
.readable_per_se(request.user)
.only("depth", "path")
.first()
)
if highest_readable is None:
raise (
@@ -881,7 +893,12 @@ class DocumentViewSet(
children = self.queryset.filter(children_clause, deleted_at__isnull=True)
queryset = ancestors.filter(depth__gte=highest_readable.depth) | children
queryset = (
ancestors.select_related("creator").filter(
depth__gte=highest_readable.depth
)
| children
)
queryset = queryset.order_by("path")
queryset = queryset.annotate_user_roles(user)
queryset = queryset.annotate_is_favorite(user)
@@ -948,33 +965,34 @@ class DocumentViewSet(
**link_kwargs,
)
# Always add the logged-in user as OWNER
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
).exclude(user=request.user)
accesses_to_create.extend(
# Always add the logged-in user as OWNER for root documents
if document.is_root():
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
user_id=access.user_id,
team=access.team,
role=access.role,
user=request.user,
role=models.RoleChoices.OWNER,
)
for access in original_accesses
)
]
# Bulk create all the duplicated accesses
models.DocumentAccess.objects.bulk_create(accesses_to_create)
# 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
).exclude(user=request.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)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
@@ -1108,15 +1126,50 @@ class DocumentViewSet(
document=document, user=user
).delete()
if deleted:
return drf.response.Response(
{"detail": "Document unmarked as favorite"},
status=drf.status.HTTP_204_NO_CONTENT,
)
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
return drf.response.Response(
{"detail": "Document was already not marked as favorite"},
status=drf.status.HTTP_200_OK,
)
@drf.decorators.action(detail=True, methods=["post", "delete"], url_path="mask")
def mask(self, request, *args, **kwargs):
"""Mask or unmask the document for the logged-in user based on the HTTP method."""
# Check permissions first
document = self.get_object()
user = request.user
try:
link_trace = models.LinkTrace.objects.get(document=document, user=user)
except models.LinkTrace.DoesNotExist:
return drf.response.Response(
{"detail": "User never accessed this document before."},
status=status.HTTP_400_BAD_REQUEST,
)
if request.method == "POST":
if link_trace.is_masked:
return drf.response.Response(
{"detail": "Document was already masked"},
status=drf.status.HTTP_200_OK,
)
link_trace.is_masked = True
link_trace.save(update_fields=["is_masked"])
return drf.response.Response(
{"detail": "Document was masked"},
status=drf.status.HTTP_201_CREATED,
)
# Handle DELETE method to unmask document
if not link_trace.is_masked:
return drf.response.Response(
{"detail": "Document was already not masked"},
status=drf.status.HTTP_200_OK,
)
link_trace.is_masked = False
link_trace.save(update_fields=["is_masked"])
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
@drf.decorators.action(detail=True, methods=["post"], url_path="attachment-upload")
def attachment_upload(self, request, *args, **kwargs):
"""Upload a file related to a given document"""
@@ -1247,7 +1300,8 @@ class DocumentViewSet(
)
attachments_documents = (
self.queryset.filter(attachments__contains=[key])
self.queryset.select_related(None)
.filter(attachments__contains=[key])
.only("path")
.order_by("path")
)
@@ -1406,6 +1460,15 @@ class DocumentViewSet(
url = unquote(url)
url_validator = URLValidator(schemes=["http", "https"])
try:
url_validator(url)
except drf.exceptions.ValidationError as e:
return drf.response.Response(
{"detail": str(e)},
status=drf.status.HTTP_400_BAD_REQUEST,
)
try:
response = requests.get(
url,
@@ -1436,10 +1499,10 @@ class DocumentViewSet(
return proxy_response
except requests.RequestException as e:
logger.error("Proxy request failed: %s", str(e))
return drf_response.Response(
{"error": f"Failed to fetch resource: {e!s}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
logger.exception(e)
return drf.response.Response(
{"error": f"Failed to fetch resource from {url}"},
status=status.HTTP_400_BAD_REQUEST,
)
+10 -1
View File
@@ -150,7 +150,7 @@ class DocumentFactory(factory.django.DjangoModelFactory):
"""Add link traces to document from a given list of users."""
if create and extracted:
for item in extracted:
models.LinkTrace.objects.create(document=self, user=item)
models.LinkTrace.objects.update_or_create(document=self, user=item)
@factory.post_generation
def favorited_by(self, create, extracted, **kwargs):
@@ -159,6 +159,15 @@ class DocumentFactory(factory.django.DjangoModelFactory):
for item in extracted:
models.DocumentFavorite.objects.create(document=self, user=item)
@factory.post_generation
def masked_by(self, create, extracted, **kwargs):
"""Mark document as masked by a list of users."""
if create and extracted:
for item in extracted:
models.LinkTrace.objects.update_or_create(
document=self, user=item, defaults={"is_masked": True}
)
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
"""Create fake document user accesses for testing."""
@@ -0,0 +1,51 @@
# Generated by Django 5.2.3 on 2025-07-13 08:22
from django.db import migrations, models
import core.validators
class Migration(migrations.Migration):
dependencies = [
("core", "0023_remove_document_is_public_and_more"),
]
operations = [
migrations.AddField(
model_name="linktrace",
name="is_masked",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="user",
name="language",
field=models.CharField(
blank=True,
choices=[
("en-us", "English"),
("fr-fr", "Français"),
("de-de", "Deutsch"),
("nl-nl", "Nederlands"),
("es-es", "Español"),
],
default=None,
help_text="The language in which the user wants to see the interface.",
max_length=10,
null=True,
verbose_name="language",
),
),
migrations.AlterField(
model_name="user",
name="sub",
field=models.CharField(
blank=True,
help_text="Required. 255 characters or fewer. ASCII characters only.",
max_length=255,
null=True,
unique=True,
validators=[core.validators.sub_validator],
verbose_name="sub",
),
),
]
+14 -15
View File
@@ -14,7 +14,7 @@ from django.contrib.auth import models as auth_models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.postgres.fields import ArrayField
from django.contrib.sites.models import Site
from django.core import mail, validators
from django.core import mail
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
@@ -39,6 +39,7 @@ from .choices import (
RoleChoices,
get_equivalent_link_definition,
)
from .validators import sub_validator
logger = getLogger(__name__)
@@ -136,22 +137,12 @@ class UserManager(auth_models.UserManager):
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
"""User model to work with OIDC only authentication."""
sub_validator = validators.RegexValidator(
regex=r"^[\w.@+-:]+\Z",
message=_(
"Enter a valid sub. This value may contain only letters, "
"numbers, and @/./+/-/_/: characters."
),
)
sub = models.CharField(
_("sub"),
help_text=_(
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
),
help_text=_("Required. 255 characters or fewer. ASCII characters only."),
max_length=255,
unique=True,
validators=[sub_validator],
unique=True,
blank=True,
null=True,
)
@@ -762,6 +753,12 @@ class Document(MP_Node, BaseModel):
can_update = (
is_owner_or_admin or role == RoleChoices.EDITOR
) and not is_deleted
can_create_children = can_update and user.is_authenticated
can_destroy = (
is_owner
if self.is_root()
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
)
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any(
@@ -784,15 +781,16 @@ class Document(MP_Node, BaseModel):
"media_check": can_get,
"can_edit": can_update,
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
"children_create": can_create_children,
"collaboration_auth": can_get,
"cors_proxy": can_get,
"descendants": can_get,
"destroy": is_owner,
"destroy": can_destroy,
"duplicate": can_get and user.is_authenticated,
"favorite": can_get and user.is_authenticated,
"link_configuration": is_owner_or_admin,
"invite_owner": is_owner,
"mask": can_get and user.is_authenticated,
"move": is_owner_or_admin and not self.ancestors_deleted_at,
"partial_update": can_update,
"restore": is_owner,
@@ -958,6 +956,7 @@ class LinkTrace(BaseModel):
related_name="link_traces",
)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="link_traces")
is_masked = models.BooleanField(default=False)
class Meta:
db_table = "impress_link_trace"
+2 -1
View File
@@ -9,7 +9,8 @@ from core import enums
AI_ACTIONS = {
"prompt": (
"Answer the prompt in markdown format. "
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
@@ -62,10 +62,14 @@ class CollaborationService:
except requests.RequestException as e:
raise requests.HTTPError("Failed to get document connection info.") from e
if response.status_code != 200:
raise requests.HTTPError(
f"Failed to get document connection info. Status code: {response.status_code}, "
f"Response: {response.text}"
)
result = response.json()
return result.get("count", 0), result.get("exists", False)
if response.status_code == 200:
result = response.json()
return result.get("count", 0), result.get("exists", False)
if response.status_code == 404:
return 0, False
raise requests.HTTPError(
f"Failed to get document connection info. Status code: {response.status_code}, "
f"Response: {response.text}"
)
@@ -175,8 +175,11 @@ def test_api_documents_ai_transform_authenticated_success(mock_create, reach, ro
{
"role": "system",
"content": (
"Answer the prompt in markdown format. Preserve the language and markdown "
"formatting. Do not provide any other information. Preserve the language."
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
},
{"role": "user", "content": "Hello"},
@@ -249,8 +252,11 @@ def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_te
{
"role": "system",
"content": (
"Answer the prompt in markdown format. Preserve the language and markdown "
"formatting. Do not provide any other information. Preserve the language."
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
},
{"role": "user", "content": "Hello"},
@@ -48,7 +48,7 @@ def test_api_documents_ask_for_access_create_authenticated():
An email should be sent to document owners and admins to notify them.
"""
owner_user = UserFactory(language="en-us")
admin_user = UserFactory(language="fr-fr")
admin_user = UserFactory(language="en-us")
document = DocumentFactory(
users=[
(owner_user, RoleChoices.OWNER),
@@ -246,3 +246,73 @@ def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_webs
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_can_edit_websocket_server_room_not_found(
settings,
):
"""
When the websocket server returns a 404, the document can be updated like if the user was
not connected to the websocket.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=404)
assert cache.get(f"docs:no-websocket:{document.id}") is None
response = client.get(
f"/api/v1.0/documents/{document.id!s}/can-edit/",
)
assert response.status_code == 200
assert response.json() == {"can_edit": True}
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_can_edit_websocket_server_room_not_found_other_already_editing(
settings,
):
"""
When the websocket server returns a 404 and another user is editing the document,
the response should be can-edit=False.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=404)
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
response = client.get(
f"/api/v1.0/documents/{document.id!s}/can-edit/",
)
assert response.status_code == 200
assert response.json() == {"can_edit": False}
assert ws_resp.call_count == 1
@@ -2,6 +2,7 @@
import pytest
import responses
from requests.exceptions import RequestException
from rest_framework.test import APIClient
from core import factories
@@ -149,3 +150,41 @@ def test_api_docs_cors_proxy_unsupported_media_type():
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 415
@pytest.mark.parametrize(
"url_to_fetch",
[
"ftp://external-url.com/assets/index.html",
"ftps://external-url.com/assets/index.html",
"invalid-url.com",
"ssh://external-url.com/assets/index.html",
],
)
def test_api_docs_cors_proxy_invalid_url(url_to_fetch):
"""Test the CORS proxy API for documents with an invalid URL."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 400
assert response.json() == ["Enter a valid URL."]
@responses.activate
def test_api_docs_cors_proxy_request_failed():
"""Test the CORS proxy API for documents with a request failed."""
document = factories.DocumentFactory(link_reach="public")
client = APIClient()
url_to_fetch = "https://external-url.com/assets/index.html"
responses.get(url_to_fetch, body=RequestException("Connection refused"))
response = client.get(
f"/api/v1.0/documents/{document.id!s}/cors-proxy/?url={url_to_fetch}"
)
assert response.status_code == 400
assert response.json() == {
"error": "Failed to fetch resource from https://external-url.com/assets/index.html"
}
@@ -148,7 +148,7 @@ def test_api_documents_create_for_owner_invalid_sub():
data = {
"title": "My Document",
"content": "Document content",
"sub": "123!!",
"sub": "invalid süb",
"email": "john.doe@example.com",
}
@@ -163,10 +163,7 @@ def test_api_documents_create_for_owner_invalid_sub():
assert not Document.objects.exists()
assert response.json() == {
"sub": [
"Enter a valid sub. This value may contain only letters, "
"numbers, and @/./+/-/_/: characters."
]
"sub": ["Enter a valid sub. This value should be ASCII only."]
}
@@ -252,3 +252,44 @@ def test_api_documents_duplicate_with_accesses_non_admin(role):
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 1
assert duplicated_accesses.get(user=user).role == "owner"
@pytest.mark.parametrize("role", ["editor", "reader"])
def test_api_documents_duplicate_non_root_document(role):
"""
Non-root documents can be duplicated but without accesses.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "owner")])
child = factories.DocumentFactory(
parent=document, users=[(user, role)], title="document with accesses"
)
assert child.accesses.count() == 1
# Duplicate the document via the API endpoint requesting to duplicate accesses
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/",
{"with_accesses": True},
format="json",
)
assert response.status_code == 201
duplicated_document = models.Document.objects.get(id=response.json()["id"])
assert duplicated_document.title == "Copy of document with accesses"
assert duplicated_document.content == child.content
assert duplicated_document.link_reach == child.link_reach
assert duplicated_document.link_role == child.link_role
assert duplicated_document.creator == user
assert duplicated_document.duplicated_from == child
assert duplicated_document.attachments == []
# No access should be created for non root documents
duplicated_accesses = duplicated_document.accesses
assert duplicated_accesses.count() == 0
assert duplicated_document.is_sibling_of(child)
assert duplicated_document.is_child_of(document)
@@ -45,7 +45,10 @@ def test_api_document_favorite_anonymous_user(method, reach):
],
)
def test_api_document_favorite_authenticated_post_allowed(reach, has_role):
"""Authenticated users should be able to mark a document as favorite using POST."""
"""
Authenticated users should be able to mark a document to which they have access
as favorite using POST.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach=reach)
client = APIClient()
@@ -69,7 +72,10 @@ def test_api_document_favorite_authenticated_post_allowed(reach, has_role):
def test_api_document_favorite_authenticated_post_forbidden():
"""Authenticated users should be able to mark a document as favorite using POST."""
"""
Authenticated users should not be allowed to mark a document to which they don't
have access as favorite using POST.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(link_reach="restricted")
client = APIClient()
@@ -41,8 +41,8 @@ def test_api_document_favorite_list_authenticated_with_favorite():
client = APIClient()
client.force_login(user)
# User don't have access to this document, let say it had access and this access has been
# removed. It should not be in the favorite list anymore.
# If the user doesn't have access to this document (e.g the user had access
# and this access was removed), it should not be in the favorite list anymore.
factories.DocumentFactory(favorited_by=[user])
document = factories.UserDocumentAccessFactory(
@@ -312,6 +312,84 @@ def test_api_documents_list_filter_is_favorite_invalid():
assert len(results) == 5
# Filters: is_masked
def test_api_documents_list_filter_is_masked_true():
"""
Authenticated users should be able to filter documents they marked as masked.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(2, users=[user])
masked_documents = factories.DocumentFactory.create_batch(
3, users=[user], masked_by=[user]
)
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
for document in unmasked_documents:
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
response = client.get("/api/v1.0/documents/?is_masked=true")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 3
# Ensure all results are marked as masked by the current user
masked_documents_ids = [str(doc.id) for doc in masked_documents]
for result in results:
assert result["id"] in masked_documents_ids
def test_api_documents_list_filter_is_masked_false():
"""
Authenticated users should be able to filter documents they didn't mark as masked.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(2, users=[user])
masked_documents = factories.DocumentFactory.create_batch(
3, users=[user], masked_by=[user]
)
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
for document in unmasked_documents:
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
response = client.get("/api/v1.0/documents/?is_masked=false")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 4
# Ensure all results are not marked as masked by the current user
masked_documents_ids = [str(doc.id) for doc in masked_documents]
for result in results:
assert result["id"] not in masked_documents_ids
def test_api_documents_list_filter_is_masked_invalid():
"""Filtering with an invalid `is_masked` value should do nothing."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(2, users=[user])
factories.DocumentFactory.create_batch(3, users=[user], masked_by=[user])
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
for document in unmasked_documents:
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
response = client.get("/api/v1.0/documents/?is_masked=invalid")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 7
# Filters: title
@@ -0,0 +1,353 @@
"""Test mask document API endpoint for users in impress's core app."""
import pytest
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize(
"reach",
[
"restricted",
"authenticated",
"public",
],
)
@pytest.mark.parametrize("method", ["post", "delete"])
def test_api_document_mask_anonymous_user(method, reach):
"""Anonymous users should not be able to mask/unmask documents."""
document = factories.DocumentFactory(link_reach=reach)
response = getattr(APIClient(), method)(
f"/api/v1.0/documents/{document.id!s}/mask/"
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
# Verify in database
assert models.LinkTrace.objects.exists() is False
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_allowed(reach, has_role):
"""Authenticated users should be able to mask a document to which they have access."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try masking the document without a link trace
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 400
assert response.json() == {"detail": "User never accessed this document before."}
assert not models.LinkTrace.objects.filter(document=document, user=user).exists()
models.LinkTrace.objects.create(document=document, user=user)
# Mask document
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 201
assert response.json() == {"detail": "Document was masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
def test_api_document_mask_authenticated_post_forbidden():
"""
Authenticated users should no be allowed to mask a document
to which they don't have access.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
# Try masking
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
# Verify in database
assert (
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
)
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_already_masked_allowed(reach, has_role):
"""POST should not create duplicate link trace if already marked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, masked_by=[user])
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 200
assert response.json() == {"detail": "Document was already masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
def test_api_document_mask_authenticated_post_already_masked_forbidden():
"""POST should not create duplicate masks if already marked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted", masked_by=[user])
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert models.LinkTrace.objects.filter(document=document, user=user).exists()
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_unmasked_allowed(reach, has_role):
"""POST should not create duplicate link trace if unmasked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 201
assert response.json() == {"detail": "Document was masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
def test_api_document_mask_authenticated_post_unmasked_forbidden():
"""POST should not create duplicate masks if unmasked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
# Try masking again
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=False
).exists()
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_delete_allowed(reach, has_role):
"""Authenticated users should be able to unmask a document using DELETE."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, masked_by=[user])
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Unmask document
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 204
assert response.content == b"" # No body
assert response.text == "" # Empty decoded text
assert "Content-Type" not in response.headers # No Content-Type for 204
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=False
).exists()
def test_api_document_mask_authenticated_delete_forbidden():
"""
Authenticated users should not be allowed to unmask a document if
they don't have access to it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted", masked_by=[user])
# Unmask document
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_delete_not_masked_allowed(reach, has_role):
"""DELETE should be idempotent if the document is not masked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
# Try unmasking the document without a link trace
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 400
assert response.json() == {"detail": "User never accessed this document before."}
assert not models.LinkTrace.objects.filter(document=document, user=user).exists()
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
# Unmask document
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 200
assert response.json() == {"detail": "Document was already not masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=False
).exists()
def test_api_document_mask_authenticated_delete_not_masked_forbidden():
"""DELETE should be idempotent if the document is not masked."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
# Try to unmask when no entry exists
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
assert (
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
)
@pytest.mark.parametrize(
"reach, has_role",
[
["restricted", True],
["authenticated", False],
["authenticated", True],
["public", False],
["public", True],
],
)
def test_api_document_mask_authenticated_post_unmark_then_mark_again_allowed(
reach, has_role
):
"""A user should be able to mask, unmask, and mask a document again."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
if has_role:
models.DocumentAccess.objects.create(document=document, user=user)
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
url = f"/api/v1.0/documents/{document.id!s}/mask/"
# Mask document
response = client.post(url)
assert response.status_code == 201
# Unmask document
response = client.delete(url)
assert response.status_code == 204
assert response.content == b"" # No body
assert response.text == "" # Empty decoded text
assert "Content-Type" not in response.headers # No Content-Type for 204
# Mask document again
response = client.post(url)
assert response.status_code == 201
assert response.json() == {"detail": "Document was masked"}
assert models.LinkTrace.objects.filter(
document=document, user=user, is_masked=True
).exists()
@@ -49,6 +49,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,
"media_auth": True,
"media_check": True,
"move": False,
@@ -121,6 +122,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"link_select_options": models.LinkReachChoices.get_select_options(
**links_definition
),
"mask": False,
"media_auth": True,
"media_check": True,
"move": False,
@@ -226,6 +228,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False,
@@ -305,6 +308,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"link_select_options": models.LinkReachChoices.get_select_options(
**links_definition
),
"mask": True,
"move": False,
"media_auth": True,
"media_check": True,
@@ -490,7 +494,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"destroy": access.role == "owner",
"destroy": access.role in ["administrator", "owner"],
"duplicate": True,
"favorite": True,
"invite_owner": access.role == "owner",
@@ -498,6 +502,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"link_select_options": models.LinkReachChoices.get_select_options(
**link_definition
),
"mask": True,
"media_auth": True,
"media_check": True,
"move": access.role in ["administrator", "owner"],
@@ -91,6 +91,7 @@ def test_api_documents_trashbin_format():
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False, # Can't move a deleted document
@@ -539,6 +539,47 @@ def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websoc
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_update_websocket_server_room_not_found_fallback_to_no_websocket_other_users(
settings,
):
"""
When the WebSocket server does not have the room created, the logic should fallback to
no-WebSocket. If another user is already editing, the update must be denied.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_document_values = serializers.DocumentSerializer(
instance=factories.DocumentFactory()
).data
new_document_values["websocket"] = False
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=404)
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
new_document_values,
format="json",
)
assert response.status_code == 403
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_update_force_websocket_param_to_true(settings):
"""
@@ -0,0 +1,44 @@
"""Test user light serializer."""
import pytest
from core import factories
from core.api.serializers import UserLightSerializer
pytestmark = pytest.mark.django_db
def test_user_light_serializer():
"""Test user light serializer."""
user = factories.UserFactory(
email="test@test.com",
full_name="John Doe",
short_name="John",
)
serializer = UserLightSerializer(user)
assert serializer.data["full_name"] == "John Doe"
assert serializer.data["short_name"] == "John"
def test_user_light_serializer_no_full_name():
"""Test user light serializer without full name."""
user = factories.UserFactory(
email="test_foo@test.com",
full_name=None,
short_name="John",
)
serializer = UserLightSerializer(user)
assert serializer.data["full_name"] == "test_foo"
assert serializer.data["short_name"] == "John"
def test_user_light_serializer_no_short_name():
"""Test user light serializer without short name."""
user = factories.UserFactory(
email="test_foo@test.com",
full_name=None,
short_name=None,
)
serializer = UserLightSerializer(user)
assert serializer.data["full_name"] == "test_foo"
assert serializer.data["short_name"] == "test_foo"
@@ -165,6 +165,7 @@ def test_models_documents_get_abilities_forbidden(
"duplicate": False,
"favorite": False,
"invite_owner": False,
"mask": False,
"media_auth": False,
"media_check": False,
"move": False,
@@ -233,6 +234,7 @@ def test_models_documents_get_abilities_reader(
"public": ["reader", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"media_auth": True,
"media_check": True,
"move": False,
@@ -297,6 +299,7 @@ def test_models_documents_get_abilities_editor(
"public": ["reader", "editor"],
"restricted": None,
},
"mask": is_authenticated,
"media_auth": True,
"media_check": True,
"move": False,
@@ -350,6 +353,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": True,
@@ -400,6 +404,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": True,
@@ -453,6 +458,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False,
@@ -513,6 +519,7 @@ def test_models_documents_get_abilities_reader_user(
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False,
@@ -571,6 +578,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"move": False,
@@ -585,6 +593,86 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
}
@pytest.mark.parametrize(
"is_authenticated, is_creator,role,link_reach,link_role,can_destroy",
[
(True, False, "owner", "restricted", "editor", True),
(True, True, "owner", "restricted", "editor", True),
(True, False, "owner", "restricted", "reader", True),
(True, True, "owner", "restricted", "reader", True),
(True, False, "owner", "authenticated", "editor", True),
(True, True, "owner", "authenticated", "editor", True),
(True, False, "owner", "authenticated", "reader", True),
(True, True, "owner", "authenticated", "reader", True),
(True, False, "owner", "public", "editor", True),
(True, True, "owner", "public", "editor", True),
(True, False, "owner", "public", "reader", True),
(True, True, "owner", "public", "reader", True),
(True, False, "administrator", "restricted", "editor", True),
(True, True, "administrator", "restricted", "editor", True),
(True, False, "administrator", "restricted", "reader", True),
(True, True, "administrator", "restricted", "reader", True),
(True, False, "administrator", "authenticated", "editor", True),
(True, True, "administrator", "authenticated", "editor", True),
(True, False, "administrator", "authenticated", "reader", True),
(True, True, "administrator", "authenticated", "reader", True),
(True, False, "administrator", "public", "editor", True),
(True, True, "administrator", "public", "editor", True),
(True, False, "administrator", "public", "reader", True),
(True, True, "administrator", "public", "reader", True),
(True, False, "editor", "restricted", "editor", False),
(True, True, "editor", "restricted", "editor", True),
(True, False, "editor", "restricted", "reader", False),
(True, True, "editor", "restricted", "reader", True),
(True, False, "editor", "authenticated", "editor", False),
(True, True, "editor", "authenticated", "editor", True),
(True, False, "editor", "authenticated", "reader", False),
(True, True, "editor", "authenticated", "reader", True),
(True, False, "editor", "public", "editor", False),
(True, True, "editor", "public", "editor", True),
(True, False, "editor", "public", "reader", False),
(True, True, "editor", "public", "reader", True),
(True, False, "reader", "restricted", "editor", False),
(True, False, "reader", "restricted", "reader", False),
(True, False, "reader", "authenticated", "editor", False),
(True, True, "reader", "authenticated", "editor", True),
(True, False, "reader", "authenticated", "reader", False),
(True, False, "reader", "public", "editor", False),
(True, True, "reader", "public", "editor", True),
(True, False, "reader", "public", "reader", False),
(False, False, None, "restricted", "editor", False),
(False, False, None, "restricted", "reader", False),
(False, False, None, "authenticated", "editor", False),
(False, False, None, "authenticated", "reader", False),
(False, False, None, "public", "editor", False),
(False, False, None, "public", "reader", False),
],
)
# pylint: disable=too-many-arguments, too-many-positional-arguments
def test_models_documents_get_abilities_children_destroy( # noqa: PLR0913
is_authenticated,
is_creator,
role,
link_reach,
link_role,
can_destroy,
):
"""For a sub document, if a user can create children, he can destroy it."""
user = factories.UserFactory() if is_authenticated else AnonymousUser()
parent = factories.DocumentFactory(link_reach=link_reach, link_role=link_role)
document = factories.DocumentFactory(
link_reach=link_reach,
link_role=link_role,
parent=parent,
creator=user if is_creator else None,
)
if is_authenticated:
factories.UserDocumentAccessFactory(document=parent, user=user, role=role)
abilities = document.get_abilities(user)
assert abilities["destroy"] is can_destroy
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.parametrize(
"is_authenticated,reach",
@@ -44,3 +44,25 @@ def test_models_users_send_mail_main_missing():
user.email_user("my subject", "my message")
assert str(excinfo.value) == "User has no email address."
@pytest.mark.parametrize(
"sub,is_valid",
[
("valid_sub.@+-:=/", True),
("invalid süb", False),
(12345, True),
],
)
def test_models_users_sub_validator(sub, is_valid):
"""The "sub" field should be validated."""
user = factories.UserFactory()
user.sub = sub
if is_valid:
user.full_clean()
else:
with pytest.raises(
ValidationError,
match=("Enter a valid sub. This value should be ASCII only."),
):
user.full_clean()
+9
View File
@@ -0,0 +1,9 @@
"""Custom validators for the core app."""
from django.core.exceptions import ValidationError
def sub_validator(value):
"""Validate that the sub is ASCII only."""
if not value.isascii():
raise ValidationError("Enter a valid sub. This value should be ASCII only.")
@@ -9,7 +9,7 @@
},
"externalLinks": [
{
"label": "Github",
"label": "GitHub",
"href": "https://github.com/suitenumerique/docs/"
},
{
+143 -143
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Me eo an aozer"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Kuzhet"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
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:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
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:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Korf"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Doare korf"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -79,7 +83,7 @@ msgstr "Lenner"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr ""
msgstr "Embanner"
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
@@ -91,11 +95,11 @@ msgstr "Perc'henn"
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr ""
msgstr "Strishaet"
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr ""
msgstr "Anavezet"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
@@ -111,11 +115,11 @@ msgstr "Bugel diwezhañ"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr ""
msgstr "Breur pe c'hoar kentañ"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr ""
msgstr "Liamm diwezhañ"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
@@ -125,279 +129,275 @@ msgstr "Kleiz"
msgid "Right"
msgstr "Dehoù"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
msgstr "alc'hwez kentañ evit an enrollañ evel UIID"
#: build/lib/core/models.py:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "krouet d'ar/al"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr ""
msgstr "deiziad hag eurvezh krouidigezh an enrolladenn"
#: build/lib/core/models.py:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "hizivaet d'ar/al"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr ""
msgstr "deiziad hag eurvezh m'eo bet hizivaet an enrolladenn"
#: build/lib/core/models.py:129 core/models.py:129
#: 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 ""
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:141 core/models.py:141
msgid "sub"
msgstr "isstrollad"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
msgid "sub"
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:159 core/models.py:159
msgid "full name"
msgstr "anv klok"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "anv berr"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
msgstr "postel identelezh"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
msgstr "postel ar merour"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "yezh"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "trevnad"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
msgstr "statud ar skipailh"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
#: build/lib/core/models.py:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr ""
msgstr "oberiant"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
#: build/lib/core/models.py:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "implijer"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "titl"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
msgstr "bomm"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
msgstr "Restr"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
msgstr "Restroù"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr ""
msgstr "Restr hep titl"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr ""
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr ""
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr ""
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr ""
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr ""
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr ""
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr ""
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr ""
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr ""
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr ""
msgstr "postel"
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr ""
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr ""
msgstr "Logo ar postel"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
@@ -407,11 +407,11 @@ msgstr "Digeriñ"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""
msgstr " Kinniget gant %(brandname)s "
+81 -81
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Ersteller bin ich"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -125,272 +129,268 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:80 core/models.py:80
#: 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:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "Erstellt"
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "Aktualisiert"
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "unter"
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
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:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
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:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
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:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
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:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
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:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "Auszug"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, 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:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, 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:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
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:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
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:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
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:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
+80 -80
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -125,272 +129,268 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr ""
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr ""
+81 -81
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Yo soy el creador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
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:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Cuerpo"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Tipo de Cuerpo"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -125,272 +129,268 @@ msgstr "Izquierda"
msgid "Right"
msgstr "Derecha"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:80 core/models.py:80
#: 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:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "creado el"
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "actualizado el"
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Introduzca un sub (UUID) válido. Este valor solo puede contener letras, números y los siguientes caracteres @/./+/-/_/:"
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "sub (UUID)"
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Requerido. 255 caracteres o menos. Letras, números y los siguientes caracteres @/./+/-/_/: solamente."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "nombre completo"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "nombre abreviado"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "correo electrónico de identidad"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "correo electrónico del administrador"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "idioma"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
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:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
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:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
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:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "rol en el equipo"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
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:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "activo"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
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:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "usuario"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "título"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "resumen"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, 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:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
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:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
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:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
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:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
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:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
+81 -81
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 11:52\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 11:45\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Je suis l'auteur"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Masqué"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
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:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Corps"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Type de corps"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -125,272 +129,268 @@ msgstr "Gauche"
msgid "Right"
msgstr "Droite"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "identifiant/id"
#: build/lib/core/models.py:80 core/models.py:80
#: 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:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "créé le"
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "mis à jour le"
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Saisissez un sous-groupe valide. Cette valeur ne peut contenir que des lettres, des chiffres et les caractères @/./+/-/_/: uniquement."
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "sous-groupe"
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_/: uniquement."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "nom complet"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "nom court"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "adresse e-mail d'identité"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "adresse e-mail de l'administrateur"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "langue"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
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:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
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:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "appareil"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
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:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "statut d'équipe"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
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:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "actif"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
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:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "utilisateur"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "titre"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "extrait"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, 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:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, 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:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
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:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
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:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
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:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
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:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, 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:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, 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:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Si ce modèle est public, utilisable par n'importe qui."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
+81 -81
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Il creatore sono io"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -125,272 +129,268 @@ msgstr "Sinistra"
msgid "Right"
msgstr "Destra"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "Id"
#: build/lib/core/models.py:80 core/models.py:80
#: 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:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "creato il"
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "aggiornato il"
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Richiesto. 255 caratteri o meno. Solo lettere, numeri e @/./+/-/_/: caratteri."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "nome completo"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "nome"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "indirizzo email di identità"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "Indirizzo email dell'amministratore"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "lingua"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
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:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
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:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
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:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "stato del personale"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
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:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "attivo"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
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:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "utente"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "titolo"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, 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:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, 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:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
+81 -81
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Ik ben Eigenaar"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriete"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document was gecreëerd voor u!"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document:"
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Text"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Text type"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -125,272 +129,268 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:80 core/models.py:80
#: 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:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "gemaakt op"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "datum en tijd wanneer dossier was gecreëerd"
#: build/lib/core/models.py:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "Laatst gewijzigd op"
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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 deze id, maar de email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/core/models.py:142 core/models.py:142
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ".Geef een valide id. De waarde mag alleen letters, nummers en @/./.+/-/_: karakters bevatten."
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "id"
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Verplicht. 255 karakters of minder. Alleen letters, nummers en @/./+/-/_/: karakters zijn toegestaan."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "volledige naam"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "gebruikersnaam"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "identiteit email adres"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "admin email adres"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "taal"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wilt zien."
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker de tijden wilt zien."
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "apparaat"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
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:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "beheerder status"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen in het admin gedeelte."
#: build/lib/core/models.py:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "actief"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
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:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "gebruiker"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "titel"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "uittreksel"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, 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:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, 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:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr "Een url bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
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:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
+100 -100
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -19,87 +19,91 @@ msgstr ""
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr ""
msgstr "Informações Pessoais"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr ""
msgstr "Permissões"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr ""
msgstr "Datas importantes"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
msgstr "Estrutura de árvore"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr ""
msgstr "Título"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr ""
msgstr "Eu sou o criador"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr ""
msgstr "Favorito"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr ""
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr ""
msgstr "A propriedade de um novo documento foi concedida a você:"
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr ""
msgstr "Corpo"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
msgstr "Tipo de corpo"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr ""
msgstr "Formato"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr ""
msgstr "cópia de {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr ""
msgstr "Leitor"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr ""
msgstr "Editor"
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr ""
msgstr "Administrador"
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr ""
msgstr "Dono"
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr ""
msgstr "Restrito"
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr ""
msgstr "Autenticado"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr ""
msgstr "Público"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
@@ -125,272 +129,268 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr ""
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr ""
@@ -0,0 +1,417 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: ru\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Личная информация"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Разрешения"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Древовидная структура"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Создатель - я"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Скрытый"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Текст сообщения"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Тип сообщения"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Читатель"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Редактор"
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Администратор"
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Владелец"
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Доступ ограничен"
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Аутентификация выполнена"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Доступно всем"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Первый потомок"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Последний потомок"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Первый предок"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Последний предок"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Слева"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Справа"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: 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:87 core/models.py:87
msgid "created on"
msgstr "создано"
#: 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:93 core/models.py:93
msgid "updated on"
msgstr "обновлено"
#: 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: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:141 core/models.py:141
msgid "sub"
msgstr "вложение"
#: 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:150 core/models.py:150
msgid "full name"
msgstr "полное имя"
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "короткое имя"
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "личный адрес электронной почты"
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "e-mail администратора"
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "язык"
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "устройство"
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Пользователь является устройством или человеком."
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "статус сотрудника"
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Может ли пользователь войти на этот административный сайт."
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "активный"
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "пользователь"
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "отрывок"
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип email"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Открыть"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, ваш новый инструмент для организации и совместного использования документов в вашей команде. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Доступ получен от %(brandname)s "
+81 -81
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Ustvaril sem jaz"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
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:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
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:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Telo"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Vrsta telesa"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -125,272 +129,268 @@ msgstr "Levo"
msgid "Right"
msgstr "Desno"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: 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:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "ustvarjen na"
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "posodobljeno dne"
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Vnesite veljavno sub. Ta vrednost lahko vsebuje samo črke, številke in znake @/./+/-/_/:."
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Obvezno. 255 znakov ali manj. Samo črke, številke in znaki @/./+/-/_/: ."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "polno ime"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "kratko ime"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "elektronski naslov identitete"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "elektronski naslov skrbnika"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "jezik"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
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:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
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:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "naprava"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "kadrovski status"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
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:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "aktivni"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
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:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "uporabnik"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "naslov"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "odlomek"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, 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:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
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:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
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:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
+80 -80
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "Skaparen är jag"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
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:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -125,272 +129,268 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "aktiv"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr ""
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
+80 -80
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -125,272 +129,268 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr ""
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr ""
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr ""
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr ""
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr ""
@@ -0,0 +1,417 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: uk\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Дозволи"
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Ієрархічна структура"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Творець — я"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Приховано"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Вміст"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Тип вмісту"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
#: core/choices.py:42
msgid "Reader"
msgstr "Читач"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Редактор"
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Адміністратор"
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
msgstr "Власник"
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Обмежено"
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
msgstr "Підтверджено"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Public"
msgstr "Публічне"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Перший нащадок"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Останній нащадок"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Перший пращур"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Останній пращур"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Ліворуч"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Праворуч"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: 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:87 core/models.py:87
msgid "created on"
msgstr "створено"
#: 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:93 core/models.py:93
msgid "updated on"
msgstr "оновлено"
#: 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: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:141 core/models.py:141
msgid "sub"
msgstr "вкладений документ"
#: 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:150 core/models.py:150
msgid "full name"
msgstr "повне ім'я"
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "коротке ім'я"
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "адреса електронної пошти особи"
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "електронна адреса адміністратора"
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "мова"
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Мова, якою користувач хоче бачити інтерфейс."
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Часовий пояс, в якому користувач хоче бачити час."
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "пристрій"
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Чи є користувач пристроєм чи реальним користувачем."
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "статус співробітника"
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Чи може користувач увійти на цей сайт адміністратора."
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "активний"
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "користувач"
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "уривок"
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип пошти"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Відкрити"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, ваш новий важливий інструмент для організації, обміну та командної співпраці над вашими документами. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Запрошення отримане від %(brandname)s "
+81 -81
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 15:21+0000\n"
"PO-Revision-Date: 2025-07-09 10:42\n"
"POT-Creation-Date: 2025-09-01 21:01+0000\n"
"PO-Revision-Date: 2025-09-04 10:03\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -43,30 +43,34 @@ msgid "Creator is me"
msgstr "创建者是我"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "收藏"
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "已为您创建了一份新文档!"
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "您已被授予新文档的所有权:"
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:943 core/api/viewsets.py:943
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -125,272 +129,268 @@ msgstr "左"
msgid "Right"
msgstr "右"
#: build/lib/core/models.py:79 core/models.py:79
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:80 core/models.py:80
#: 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:86 core/models.py:86
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "创建时间"
#: build/lib/core/models.py:87 core/models.py:87
#: 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:92 core/models.py:92
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "更新时间"
#: build/lib/core/models.py:93 core/models.py:93
#: 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:129 core/models.py:129
#: 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
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "请输入有效的 sub。该值只能包含字母、数字及 @/./+/-/_/: 字符。"
#: build/lib/core/models.py:148 core/models.py:148
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "sub"
#: build/lib/core/models.py:150 core/models.py:150
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "必填。最多 255 个字符,仅允许字母、数字及 @/./+/-/_/: 字符。"
#: 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:159 core/models.py:159
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "全名"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "简称"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "身份电子邮件地址"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "管理员电子邮件地址"
#: build/lib/core/models.py:174 core/models.py:174
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "语言"
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "用户希望看到的界面语言。"
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "用户查看时间希望的时区。"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "设备"
#: build/lib/core/models.py:188 core/models.py:188
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "用户是设备还是真实用户。"
#: build/lib/core/models.py:191 core/models.py:191
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "员工状态"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "用户是否可以登录该管理员站点。"
#: build/lib/core/models.py:196 core/models.py:196
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "激活"
#: build/lib/core/models.py:199 core/models.py:199
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
#: build/lib/core/models.py:211 core/models.py:211
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "用户"
#: build/lib/core/models.py:212 core/models.py:212
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:368 build/lib/core/models.py:1281
#: core/models.py:368 core/models.py:1281
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
msgid "title"
msgstr "标题"
#: build/lib/core/models.py:369 core/models.py:369
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:418 core/models.py:418
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "文档"
#: build/lib/core/models.py:419 core/models.py:419
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:431 build/lib/core/models.py:820 core/models.py:431
#: core/models.py:820
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:855 core/models.py:855
#: build/lib/core/models.py:853 core/models.py:853
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:859 core/models.py:859
#: build/lib/core/models.py:857 core/models.py:857
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:865 core/models.py:865
#: build/lib/core/models.py:863 core/models.py:863
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:963 core/models.py:963
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:965 core/models.py:965
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:971 core/models.py:971
#: build/lib/core/models.py:970 core/models.py:970
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:993 core/models.py:993
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:995 core/models.py:995
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1001 core/models.py:1001
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1022 core/models.py:1022
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1024 core/models.py:1024
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1030 core/models.py:1030
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1036 core/models.py:1036
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1187 core/models.py:1187
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1189 core/models.py:1189
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1195 core/models.py:1195
#: build/lib/core/models.py:1194 core/models.py:1194
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1260 core/models.py:1260
#: build/lib/core/models.py:1259 core/models.py:1259
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1264 core/models.py:1264
#: build/lib/core/models.py:1263 core/models.py:1263
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1269 core/models.py:1269
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1281 core/models.py:1281
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1284 core/models.py:1284
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1288 core/models.py:1288
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1293 core/models.py:1293
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1295 core/models.py:1295
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1347 core/models.py:1347
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1349 core/models.py:1349
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1355 core/models.py:1355
#: build/lib/core/models.py:1354 core/models.py:1354
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1361 core/models.py:1361
#: build/lib/core/models.py:1360 core/models.py:1360
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1438 core/models.py:1438
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1458 core/models.py:1458
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1478 core/models.py:1478
#: build/lib/core/models.py:1477 core/models.py:1477
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
+9 -9
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.3.0"
version = "3.6.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.13.4",
"boto3==1.39.3",
"boto3==1.39.4",
"Brotli==1.1.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
@@ -34,7 +34,7 @@ dependencies = [
"django-countries==7.6.1",
"django-csp==4.0",
"django-filter==25.1",
"django-lasuite[all]==0.0.10",
"django-lasuite[all]==0.0.11",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
@@ -52,9 +52,9 @@ dependencies = [
"markdown==3.8.2",
"mozilla-django-oidc==4.0.1",
"nested-multipart-parser==1.5.0",
"openai==1.93.0",
"openai==1.95.0",
"psycopg[binary]==3.2.9",
"pycrdt==0.12.23",
"pycrdt==0.12.25",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"redis<6.0.0",
@@ -64,10 +64,10 @@ dependencies = [
]
[project.urls]
"Bug Tracker" = "https://github.com/numerique-gouv/impress/issues/new"
"Changelog" = "https://github.com/numerique-gouv/impress/blob/main/CHANGELOG.md"
"Homepage" = "https://github.com/numerique-gouv/impress"
"Repository" = "https://github.com/numerique-gouv/impress"
"Bug Tracker" = "https://github.com/suitenumerique/docs/issues/new"
"Changelog" = "https://github.com/suitenumerique/docs/blob/main/CHANGELOG.md"
"Homepage" = "https://github.com/suitenumerique/docs"
"Repository" = "https://github.com/suitenumerique/docs"
[project.optional-dependencies]
dev = [
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/');
await expect(
page.locator('header').first().locator('h2').getByText('Docs'),
page.locator('header').first().locator('h1').getByText('Docs'),
).toBeVisible();
await page.goto('unknown-page404');
});
@@ -1,6 +1,6 @@
import { FullConfig, FullProject, chromium, expect } from '@playwright/test';
import { keyCloakSignIn } from './common';
import { keyCloakSignIn } from './utils-common';
const saveStorageState = async (
browserConfig: FullProject<unknown, unknown>,
@@ -23,7 +23,7 @@ const saveStorageState = async (
page.locator('header').first().getByRole('button', {
name: 'Logout',
}),
).toBeVisible();
).toBeVisible({ timeout: 10000 });
await page.context().storageState({
path: storageState as string,
@@ -2,7 +2,7 @@ import path from 'path';
import { expect, test } from '@playwright/test';
import { CONFIG, createDoc, overrideConfig } from './common';
import { CONFIG, createDoc, overrideConfig } from './utils-common';
test.describe('Config', () => {
test('it checks that sentry is trying to init from config endpoint', async ({
@@ -43,7 +43,9 @@ test.describe('Config', () => {
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
@@ -6,7 +6,7 @@ import {
keyCloakSignIn,
randomName,
verifyDocName,
} from './common';
} from './utils-common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -22,13 +22,57 @@ test.describe('Doc Create', () => {
);
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(docsGrid.getByText(docTitle)).toBeVisible();
});
test('it creates a sub doc from slash menu editor', async ({
page,
browserName,
}) => {
const [title] = await createDoc(page, 'my-new-slash-doc', browserName, 1);
await verifyDocName(page, title);
await page.locator('.bn-block-outer').last().fill('/');
await page
.getByText('New sub-doc', {
exact: true,
})
.click();
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
});
test('it creates a sub doc from interlinking dropdown', async ({
page,
browserName,
}) => {
const [title] = await createDoc(page, 'my-new-slash-doc', browserName, 1);
await verifyDocName(page, title);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
await page
.locator('.quick-search-container')
.getByText('New sub-doc')
.click();
const input = page.getByRole('textbox', { name: 'doc title input' });
await expect(input).toHaveText('');
await expect(
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
).toBeVisible();
});
});
test.describe('Doc Create: Not logged', () => {
@@ -9,7 +9,8 @@ import {
mockedDocument,
overrideConfig,
verifyDocName,
} from './common';
} from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -92,7 +93,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
// When the visibility is changed, the ws should close the connection (backend signal)
const wsClosePromise = webSocket.waitForEvent('close');
@@ -271,7 +272,9 @@ test.describe('Doc Editor', () => {
path.join(__dirname, 'assets/logo-suite-numerique.png'),
);
const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
@@ -283,6 +286,11 @@ test.describe('Doc Editor', () => {
expect(await image.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
);
await expect(image).toHaveAttribute('role', 'presentation');
await expect(image).toHaveAttribute('alt', '');
await expect(image).toHaveAttribute('tabindex', '-1');
await expect(image).toHaveAttribute('aria-hidden', 'true');
});
test('it checks the AI buttons', async ({ page, browserName }) => {
@@ -524,6 +532,8 @@ test.describe('Doc Editor', () => {
page,
browserName,
}) => {
test.slow();
/**
* The good port is 4444, but we want to simulate a not connected
* collaborative server.
@@ -536,7 +546,12 @@ test.describe('Doc Editor', () => {
await page.goto('/');
const [title] = await createDoc(page, 'editing-blocking', browserName, 1);
const [parentTitle] = await createDoc(
page,
'editing-blocking',
browserName,
1,
);
const card = page.getByLabel('It is the card information');
await expect(
@@ -553,7 +568,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Visibility', { exact: true }).click();
await page.getByTestId('doc-visibility').click();
await page
.getByRole('menuitem', {
@@ -565,18 +580,26 @@ test.describe('Doc Editor', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByLabel('Visibility mode').click();
await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
const urlParentDoc = page.url();
const { name: childTitle } = await createRootSubPage(
page,
browserName,
'editing-blocking - child',
);
let responseCanEdit = await responseCanEditPromise;
expect(responseCanEdit.ok()).toBeTruthy();
let jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
expect(jsonCanEdit.can_edit).toBeTruthy();
const urlDoc = page.url();
const urlChildDoc = page.url();
/**
* We open another browser that will connect to the collaborative server
@@ -603,14 +626,14 @@ test.describe('Doc Editor', () => {
},
);
await otherPage.goto(urlDoc);
await otherPage.goto(urlChildDoc);
const webSocket = await webSocketPromise;
expect(webSocket.url()).toContain(
'ws://localhost:4444/collaboration/ws/?room=',
);
await verifyDocName(otherPage, title);
await verifyDocName(otherPage, childTitle);
await page.reload();
@@ -633,26 +656,21 @@ test.describe('Doc Editor', () => {
await expect(editor).toHaveAttribute('contenteditable', 'false');
await page.goto(urlParentDoc);
await verifyDocName(page, parentTitle);
await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Visibility mode').click();
await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Reading' }).click();
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
await page.reload();
await page.goto(urlChildDoc);
responseCanEditPromise = page.waitForResponse(
(response) =>
response.url().includes(`/can-edit/`) && response.status() === 200,
);
responseCanEdit = await responseCanEditPromise;
expect(responseCanEdit.ok()).toBeTruthy();
jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
expect(jsonCanEdit.can_edit).toBeTruthy();
await expect(editor).toHaveAttribute('contenteditable', 'true');
await expect(
card.getByText('Others are editing. Your network prevent changes.'),
@@ -695,4 +713,75 @@ test.describe('Doc Editor', () => {
'pink',
);
});
test('it checks interlink feature', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const { name: docChild1 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-1',
);
await verifyDocName(page, docChild1);
const { name: docChild2 } = await createRootSubPage(
page,
browserName,
'doc-interlink-child-2',
);
await verifyDocName(page, docChild2);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
const input = page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
);
const searchContainer = page.locator('.quick-search-container');
await input.fill('doc-interlink');
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
await expect(searchContainer.getByText(docChild1)).toBeVisible();
await expect(searchContainer.getByText(docChild2)).toBeVisible();
await input.pressSequentially('-child');
await expect(searchContainer.getByText(docChild1)).toBeVisible();
await expect(searchContainer.getByText(docChild2)).toBeVisible();
await expect(searchContainer.getByText(randomDoc)).toBeHidden();
// use keydown to select the second result
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
const interlink = page.getByRole('link', {
name: 'child-2',
});
await expect(interlink).toBeVisible();
await interlink.click();
await verifyDocName(page, docChild2);
});
test('it checks interlink shortcut @', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const editor = page.locator('.bn-block-outer').last();
await editor.click();
await page.keyboard.press('@');
await expect(
page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
),
).toBeVisible();
});
});
@@ -4,7 +4,14 @@ import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import pdf from 'pdf-parse';
import { createDoc, verifyDocName } from './common';
import {
TestLanguage,
createDoc,
randomName,
verifyDocName,
waitForLanguageSwitch,
} from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -18,7 +25,7 @@ test.describe('Doc Export', () => {
await createDoc(page, 'doc-editor', browserName, 1);
await page
.getByRole('button', {
name: 'download',
name: 'Export the document',
})
.click();
@@ -71,8 +78,7 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'download',
exact: true,
name: 'Export the document',
})
.click();
@@ -115,14 +121,15 @@ test.describe('Doc Export', () => {
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page.getByRole('img', { name: 'test.svg' });
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
await page
.getByRole('button', {
name: 'download',
exact: true,
name: 'Export the document',
})
.click();
@@ -175,7 +182,9 @@ test.describe('Doc Export', () => {
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page.getByRole('img', { name: 'test.svg' });
const image = page
.locator('.--docs--editor-container img.bn-visual-media')
.first();
await expect(image).toBeVisible();
@@ -189,7 +198,7 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'download',
name: 'Export the document',
})
.click();
@@ -266,7 +275,7 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'download',
name: 'Export the document',
})
.click();
@@ -316,8 +325,7 @@ test.describe('Doc Export', () => {
await page
.getByRole('button', {
name: 'download',
exact: true,
name: 'Export the document',
})
.click();
@@ -346,4 +354,201 @@ test.describe('Doc Export', () => {
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('Hello World');
});
test('it exports the doc with multi columns', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(
page,
'doc-multi-columns',
browserName,
1,
);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Three Columns', { exact: true }).click();
await page.locator('.bn-block-column').first().fill('Column 1');
await page.locator('.bn-block-column').nth(1).fill('Column 2');
await page.locator('.bn-block-column').last().fill('Column 3');
expect(await page.locator('.bn-block-column').count()).toBe(3);
await expect(
page.locator('.bn-block-column[data-node-type="column"]').first(),
).toHaveText('Column 1');
await expect(
page.locator('.bn-block-column[data-node-type="column"]').nth(1),
).toHaveText('Column 2');
await expect(
page.locator('.bn-block-column[data-node-type="column"]').last(),
).toHaveText('Column 3');
await page
.getByRole('button', {
name: 'Export the document',
})
.click();
await expect(
page.getByRole('button', {
name: 'Download',
exact: true,
}),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('Column 1');
expect(pdfData.text).toContain('Column 2');
expect(pdfData.text).toContain('Column 3');
});
test('it injects the correct language attribute into PDF export', async ({
page,
browserName,
}) => {
await waitForLanguageSwitch(page, TestLanguage.French);
// Wait for the page to be ready after language switch
await page.waitForLoadState('domcontentloaded');
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
const randomDocFrench = randomName(
'doc-language-export-french',
browserName,
1,
)[0];
await page
.getByRole('button', {
name: 'Nouveau doc',
})
.click();
await page.waitForURL('**/docs/**', {
timeout: 10000,
waitUntil: 'domcontentloaded',
});
const input = page.getByLabel('doc title input');
await expect(input).toBeVisible();
await expect(input).toHaveText('');
await input.click();
await input.fill(randomDocFrench);
await input.blur();
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.fill('Contenu de test pour export en français');
await page
.getByRole('button', {
name: 'Exporter le document',
})
.click();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
void page
.getByRole('button', {
name: 'Télécharger',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfString = pdfBuffer.toString('latin1');
expect(pdfString).toContain('/Lang (fr)');
});
test('it exports the doc with interlinking', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(
page,
'export-interlinking',
browserName,
1,
);
await verifyDocName(page, randomDoc);
const { name: docChild } = await createRootSubPage(
page,
browserName,
'export-interlink-child',
);
await verifyDocName(page, docChild);
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
await page
.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
)
.fill('interlink-child');
await page
.locator('.quick-search-container')
.getByText('interlink-child')
.click();
const interlink = page.getByRole('link', {
name: 'interlink-child',
});
await expect(interlink).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${docChild}.pdf`);
});
await page
.getByRole('button', {
name: 'Export the document',
})
.click();
void page
.getByRole('button', {
name: 'Download',
exact: true,
})
.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('interlink-child'); // This is the pdf text
});
});
@@ -1,15 +1,16 @@
import { expect, test } from '@playwright/test';
import { createDoc, mockedListDocs } from './common';
import { createDoc, mockedListDocs } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc grid dnd', () => {
test('it creates a doc', async ({ page, browserName }) => {
await page.goto('/');
const header = page.locator('header').first();
await createDoc(page, 'Draggable doc', browserName, 1);
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
await createDoc(page, 'Droppable doc', browserName, 1);
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
const response = await page.waitForResponse(
(response) =>
@@ -165,6 +166,40 @@ test.describe('Doc grid dnd', () => {
});
});
test.describe('Doc grid dnd mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test('DND is deactivated on mobile', async ({ page, browserName }) => {
await page.goto('/');
const docsGrid = page.getByTestId('docs-grid');
await expect(page.getByTestId('docs-grid')).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
await expect(docsGrid.getByRole('row').first()).toBeVisible();
await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0);
await createDoc(page, 'Draggable doc mobile', browserName, 1, true);
await createRootSubPage(
page,
browserName,
'Draggable doc mobile child',
true,
);
await page
.getByRole('button', { name: 'Open the header menu' })
.getByText('menu')
.click();
await expect(page.locator('.--docs-sub-page-item').first()).toHaveAttribute(
'draggable',
'false',
);
});
});
const data = [
{
id: 'can-drop-and-drag',
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, getGridRow } from './common';
import { createDoc, getGridRow } from './utils-common';
type SmallDoc = {
id: string;
@@ -91,6 +91,22 @@ test.describe('Document grid item options', () => {
await page.goto('/');
});
test('it checks the share modal', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `check share modal`, browserName);
await page.goto('/');
await expect(page.getByText(docTitle)).toBeVisible();
const row = await getGridRow(page, docTitle);
await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Share' }).click();
await expect(
page.getByRole('dialog').getByText('Share the document'),
).toBeVisible();
});
test('it pins a document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `Favorite doc`, browserName);
@@ -103,7 +119,7 @@ test.describe('Document grid item options', () => {
await page.getByText('push_pin').click();
// Check is pinned
await expect(row.getByLabel('Pin document icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -112,7 +128,7 @@ test.describe('Document grid item options', () => {
await page.getByText('Unpin').click();
// Check is unpinned
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -211,18 +227,18 @@ test.describe('Documents filters', () => {
// Initial state
await expect(allDocs).toBeVisible();
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
await expect(allDocs).toHaveAttribute('aria-current', 'page');
await expect(myDocs).toBeVisible();
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
await expect(myDocs).not.toHaveAttribute('aria-current');
await expect(sharedWithMe).toBeVisible();
await expect(sharedWithMe).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
await allDocs.click();
@@ -4,11 +4,11 @@ import {
createDoc,
getGridRow,
goToGridDoc,
mockedAccesses,
mockedDocument,
mockedInvitations,
verifyDocName,
} from './common';
} from './utils-common';
import { mockedAccesses, mockedInvitations } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -30,7 +30,7 @@ test.describe('Doc Header', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Visibility', { exact: true }).click();
await page.getByTestId('doc-visibility').click();
await page
.getByRole('menuitem', {
@@ -44,7 +44,9 @@ test.describe('Doc Header', () => {
await expect(card.getByText('Owner ·')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Open the document options' }),
).toBeVisible();
@@ -59,6 +61,31 @@ test.describe('Doc Header', () => {
await verifyDocName(page, 'Hello World');
});
test('it updates the title doc adding a leading emoji', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
await expect(docTitle).toBeVisible();
await docTitle.fill('👍 Hello Emoji World');
await docTitle.blur();
await verifyDocName(page, '👍 Hello Emoji World');
// Check the tree
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText('Hello Emoji World')).toBeVisible();
await expect(docTree.getByLabel('Document emoji icon')).toBeVisible();
await expect(docTree.getByLabel('Simple document icon')).toBeHidden();
await page.getByTestId('home-button').click();
// Check the documents grid
const gridRow = await getGridRow(page, 'Hello Emoji World');
await expect(gridRow.getByLabel('Document emoji icon')).toBeVisible();
await expect(gridRow.getByLabel('Simple document icon')).toBeHidden();
});
test('it deletes the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
@@ -115,7 +142,9 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();
await page.getByLabel('Open the document options').click();
@@ -185,7 +214,9 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(page.getByLabel('Delete document')).toBeDisabled();
@@ -245,7 +276,9 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Export the document' }),
).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(page.getByLabel('Delete document')).toBeDisabled();
@@ -409,7 +442,7 @@ test.describe('Doc Header', () => {
const row = await getGridRow(page, docTitle);
// Check is pinned
await expect(row.getByLabel('Pin document icon')).toBeVisible();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -424,7 +457,7 @@ test.describe('Doc Header', () => {
await page.goto('/');
// Check is unpinned
await expect(row.getByLabel('Pin document icon')).toBeHidden();
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -442,9 +475,10 @@ test.describe('Doc Header', () => {
page.getByText('Document duplicated successfully!'),
).toBeVisible();
await page.goto('/');
const duplicateTitle = 'Copy of ' + docTitle;
await verifyDocName(page, duplicateTitle);
await page.goto('/');
const row = await getGridRow(page, duplicateTitle);
@@ -456,6 +490,40 @@ test.describe('Doc Header', () => {
await page.getByText(duplicateDuplicateTitle).click();
await expect(page.getByText('Hello Duplicated World')).toBeVisible();
});
test('it duplicates a child document', async ({ page, browserName }) => {
await createDoc(page, `Duplicate doc`, browserName);
const { name: childTitle } = await createRootSubPage(
page,
browserName,
'Duplicate doc - child',
);
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Hello Duplicated World');
const duplicateTitle = 'Copy of ' + childTitle;
const docTree = page.getByTestId('doc-tree');
const child = docTree
.getByRole('treeitem')
.locator('.--docs-sub-page-item')
.filter({
hasText: childTitle,
});
await child.hover();
await child.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await verifyDocName(page, duplicateTitle);
await expect(
page.getByTestId('doc-tree').getByText(duplicateTitle),
).toBeVisible();
});
});
test.describe('Documents Header mobile', () => {
@@ -1,8 +1,8 @@
import { expect, test } from '@playwright/test';
import { createDoc, verifyDocName } from './common';
import { updateShareLink } from './share-utils';
import { createRootSubPage } from './sub-pages-utils';
import { createDoc, verifyDocName } from './utils-common';
import { updateShareLink } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Inherited share accesses', () => {
test('it checks inherited accesses', async ({ page, browserName }) => {
@@ -31,9 +31,7 @@ test.describe('Inherited share accesses', () => {
await verifyDocName(page, parentTitle);
});
});
test.describe('Inherited share link', () => {
test('it checks if the link is inherited', async ({ page, browserName }) => {
await page.goto('/');
// Create root doc
@@ -47,12 +45,52 @@ test.describe('Inherited share link', () => {
// Create sub page
await createRootSubPage(page, browserName, 'sub-page');
// // verify share link is restricted and reader
// Verify share link is like the parent document
await page.getByRole('button', { name: 'Share' }).click();
// await expect(page.getByText('Inherited share')).toBeVisible();
const docVisibilityCard = page.getByLabel('Doc visibility card');
await expect(docVisibilityCard).toBeVisible();
await expect(docVisibilityCard.getByText('Connected')).toBeVisible();
await expect(docVisibilityCard.getByText('Reading')).toBeVisible();
await docVisibilityCard.getByText('Reading').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await expect(docVisibilityCard.getByText('Reading')).toBeHidden();
await expect(docVisibilityCard.getByText('Editing')).toBeVisible();
// Verify inherited link
await docVisibilityCard.getByText('Connected').click();
await expect(
page.getByRole('menuitem', { name: 'Private' }),
).toBeDisabled();
// Update child link
await page.getByRole('menuitem', { name: 'Public' }).click();
await expect(docVisibilityCard.getByText('Connected')).toBeHidden();
await expect(
docVisibilityCard.getByText('Public', {
exact: true,
}),
).toBeVisible();
await expect(
docVisibilityCard.getByText(
'The link sharing rules differ from the parent document',
),
).toBeVisible();
// Restore inherited link
await page.getByRole('button', { name: 'Restore' }).click();
await expect(docVisibilityCard.getByText('Connected')).toBeVisible();
await expect(docVisibilityCard.getByText('Reading')).toBeVisible();
await expect(docVisibilityCard.getByText('Public')).toBeHidden();
await expect(docVisibilityCard.getByText('Editing')).toBeHidden();
await expect(
docVisibilityCard.getByText(
'The link sharing rules differ from the parent document',
),
).toBeHidden();
});
});
@@ -6,8 +6,8 @@ import {
keyCloakSignIn,
randomName,
verifyDocName,
} from './common';
import { createRootSubPage } from './sub-pages-utils';
} from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Document create member', () => {
test.beforeEach(async ({ page }) => {
@@ -244,9 +244,7 @@ test.describe('Document create member: Multiple login', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
@@ -271,9 +269,7 @@ test.describe('Document create member: Multiple login', () => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
@@ -334,9 +330,7 @@ test.describe('Document create member: Multiple login', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
@@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
import { addNewMember, createDoc, goToGridDoc, verifyDocName } from './common';
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
import { addNewMember } from './utils-share';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -148,7 +149,11 @@ test.describe('Document list members', () => {
`You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`,
);
await expect(soloOwner).toBeVisible();
await list.click();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
const newUserEmail = await addNewMember(page, 0, 'Owner');
const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`);
const newUserRoles = newUser.getByLabel('doc-role-dropdown');
@@ -157,10 +162,16 @@ test.describe('Document list members', () => {
await currentUserRole.click();
await expect(soloOwner).toBeHidden();
await list.click();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
await newUserRoles.click();
await list.click();
await list.click({
// eslint-disable-next-line playwright/no-force-option
force: true, // Force click to close the dropdown
});
await currentUserRole.click();
await page.getByLabel('Administrator').click();
@@ -8,25 +8,19 @@ import {
keyCloakSignIn,
mockedDocument,
verifyDocName,
} from './common';
} from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc Routing', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('Check the presence of the meta tag noindex', async ({ page }) => {
const buttonCreateHomepage = page.getByRole('button', {
name: 'New doc',
});
await expect(buttonCreateHomepage).toBeVisible();
await buttonCreateHomepage.click();
await expect(
page.getByRole('button', {
name: 'Share',
}),
).toBeVisible();
test('Check the presence of the meta tag noindex', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-routing-test', browserName, 1);
const metaDescription = page.locator('meta[name="robots"]');
await expect(metaDescription).toHaveAttribute('content', 'noindex');
});
@@ -60,16 +54,20 @@ test.describe('Doc Routing', () => {
});
test('checks 401 on docs/[id] page', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, '401-doc', browserName, 1);
const [docTitle] = await createDoc(page, '401-doc-parent', browserName, 1);
await verifyDocName(page, docTitle);
await createRootSubPage(page, browserName, '401-doc-child');
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
const responsePromise = page.route(
/.*\/link-configuration\/$|users\/me\/$/,
/.*\/documents\/.*\/$|users\/me\/$/,
async (route) => {
const request = route.request();
if (
request.method().includes('PUT') ||
request.method().includes('PATCH') ||
request.method().includes('GET')
) {
await route.fulfill({
@@ -84,15 +82,13 @@ test.describe('Doc Routing', () => {
},
);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
await selectVisibility.click();
await page.getByLabel('Connected').click();
await page.getByRole('link', { name: '401-doc-parent' }).click();
await responsePromise;
await expect(page.getByText('Log in to access the document')).toBeVisible();
await expect(page.getByText('Log in to access the document')).toBeVisible({
timeout: 10000,
});
});
});
@@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
import { createDoc, randomName, verifyDocName } from './common';
import { createDoc, verifyDocName } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -25,10 +26,7 @@ test.describe('Document search', () => {
);
await verifyDocName(page, doc2Title);
await page.goto('/');
await page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' })
.click();
await page.getByTestId('search-docs-button').click();
await expect(
page.getByRole('img', { name: 'No active search' }),
@@ -98,39 +96,44 @@ test.describe('Document search', () => {
).toBeHidden();
});
test("it checks we don't see filters in search modal", async ({ page }) => {
const searchButton = page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' });
await expect(searchButton).toBeVisible();
await page.getByRole('button', { name: 'search', exact: true }).click();
await expect(
page.getByRole('combobox', { name: 'Quick search input' }),
).toBeVisible();
await expect(page.getByTestId('doc-search-filters')).toBeHidden();
});
});
test.describe('Sub page search', () => {
test('it check the presence of filters in search modal', async ({
page,
browserName,
}) => {
await page.goto('/');
const [doc1Title] = await createDoc(
page,
'My sub page search',
browserName,
1,
);
await verifyDocName(page, doc1Title);
const searchButton = page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' });
await searchButton.click();
// Doc grid filters are not visible
const searchButton = page.getByTestId('search-docs-button');
const filters = page.getByTestId('doc-search-filters');
await searchButton.click();
await expect(
page.getByRole('combobox', { name: 'Quick search input' }),
).toBeVisible();
await expect(filters).toBeHidden();
await page.getByRole('button', { name: 'close' }).click();
// Create a doc without children for the moment
// and check that filters are not visible
const [doc1Title] = await createDoc(page, 'My page search', browserName, 1);
await verifyDocName(page, doc1Title);
await searchButton.click();
await expect(
page.getByRole('combobox', { name: 'Quick search input' }),
).toBeVisible();
await expect(filters).toBeHidden();
await page.getByRole('button', { name: 'close' }).click();
// Create a sub page
// and check that filters are visible
await createRootSubPage(page, browserName, 'My sub page search');
await searchButton.click();
await expect(filters).toBeVisible();
await filters.click();
await filters.getByRole('button', { name: 'Current doc' }).click();
await expect(
@@ -139,43 +142,67 @@ test.describe('Sub page search', () => {
await expect(
page.getByRole('menuitem', { name: 'Current doc' }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'Current doc' }).click();
await page.getByRole('menuitem', { name: 'All docs' }).click();
await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible();
});
test('it searches sub pages', async ({ page, browserName }) => {
await page.goto('/');
const [doc1Title] = await createDoc(
// First doc
const [firstDocTitle] = await createDoc(
page,
'My sub page search',
'My first sub page search',
browserName,
1,
);
await verifyDocName(page, doc1Title);
await page.getByRole('button', { name: 'New doc' }).click();
await verifyDocName(page, '');
await page.getByRole('textbox', { name: 'doc title input' }).click();
await page
.getByRole('textbox', { name: 'doc title input' })
.press('ControlOrMeta+a');
const [randomDocName] = randomName('doc-sub-page', browserName, 1);
await page
.getByRole('textbox', { name: 'doc title input' })
.fill(randomDocName);
const searchButton = page
.getByTestId('left-panel-desktop')
.getByRole('button', { name: 'search' });
await verifyDocName(page, firstDocTitle);
// Create a new doc - for the moment without children
const [secondDocTitle] = await createDoc(
page,
'My second sub page search',
browserName,
1,
);
const searchButton = page.getByTestId('search-docs-button');
await searchButton.click();
await expect(
page.getByRole('button', { name: 'Current doc' }),
).toBeVisible();
await page.getByRole('combobox', { name: 'Quick search input' }).click();
await page
.getByRole('combobox', { name: 'Quick search input' })
.fill('sub');
await expect(page.getByLabel(randomDocName)).toBeVisible();
.fill('sub page search');
// Expect to find the first doc
await expect(
page.getByRole('presentation').getByLabel(firstDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(secondDocTitle),
).toBeVisible();
await page.getByRole('button', { name: 'close' }).click();
// Create a sub page
const { name: secondChildDocTitle } = await createRootSubPage(
page,
browserName,
'second - Child doc',
);
await searchButton.click();
await page
.getByRole('combobox', { name: 'Quick search input' })
.fill('second');
// Now there is a sub page - expect to have the focus on the current doc
await expect(
page.getByRole('presentation').getByLabel(secondDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(secondChildDocTitle),
).toBeVisible();
await expect(
page.getByRole('presentation').getByLabel(firstDocTitle),
).toBeHidden();
});
});
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, verifyDocName } from './common';
import { createDoc, verifyDocName } from './utils-common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -2,15 +2,15 @@
import { expect, test } from '@playwright/test';
import {
addNewMember,
createDoc,
expectLoginPage,
keyCloakSignIn,
randomName,
updateDocTitle,
verifyDocName,
} from './common';
import { clickOnAddRootSubPage, createRootSubPage } from './sub-pages-utils';
} from './utils-common';
import { addNewMember } from './utils-share';
import { clickOnAddRootSubPage, createRootSubPage } from './utils-sub-pages';
test.describe('Doc Tree', () => {
test.beforeEach(async ({ page }) => {
@@ -25,7 +25,7 @@ test.describe('Doc Tree', () => {
1,
);
await verifyDocName(page, titleParent);
const addButton = page.getByRole('button', { name: 'New doc' });
const addButton = page.getByTestId('new-doc-button');
const docTree = page.getByTestId('doc-tree');
await expect(addButton).toBeVisible();
@@ -63,7 +63,7 @@ test.describe('Doc Tree', () => {
test('check the reorder of sub pages', async ({ page, browserName }) => {
await createDoc(page, 'doc-tree-content', browserName, 1);
const addButton = page.getByRole('button', { name: 'New doc' });
const addButton = page.getByTestId('new-doc-button');
await expect(addButton).toBeVisible();
const docTree = page.getByTestId('doc-tree');
@@ -201,7 +201,7 @@ test.describe('Doc Tree', () => {
).not.toHaveText(docChild);
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
await header.locator('h1').getByText('Docs').click();
await expect(page.getByText(docChild)).toBeVisible();
});
@@ -259,6 +259,10 @@ test.describe('Doc Tree: Inheritance', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('A child inherit from the parent', async ({ page, browserName }) => {
// test.slow() to extend timeout since this scenario chains Keycloak login + redirects,
// doc creation/navigation and async doc-tree loading (/documents/:id/tree), which can exceed 30s (especially in CI).
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -271,7 +275,7 @@ test.describe('Doc Tree: Inheritance', () => {
await verifyDocName(page, docParent);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await page
@@ -307,6 +311,7 @@ test.describe('Doc Tree: Inheritance', () => {
await expect(page.locator('h2').getByText(docChild)).toBeVisible();
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible({ timeout: 10000 });
await expect(docTree.getByText(docParent)).toBeVisible();
});
});
@@ -5,7 +5,7 @@ import {
goToGridDoc,
mockedDocument,
verifyDocName,
} from './common';
} from './utils-common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -6,8 +6,8 @@ import {
expectLoginPage,
keyCloakSignIn,
verifyDocName,
} from './common';
import { createRootSubPage } from './sub-pages-utils';
} from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc Visibility', () => {
test.beforeEach(async ({ page }) => {
@@ -41,7 +41,7 @@ test.describe('Doc Visibility', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
await expect(selectVisibility.getByText('Private')).toBeVisible();
@@ -51,13 +51,13 @@ test.describe('Doc Visibility', () => {
await selectVisibility.click();
await page.getByLabel('Connected').click();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await selectVisibility.click();
await page.getByLabel('Public', { exact: true }).click();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
});
});
@@ -122,9 +122,7 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
@@ -178,9 +176,7 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
@@ -209,7 +205,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await page
@@ -222,8 +218,8 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await expect(page.getByLabel('Visibility mode')).toBeVisible();
await page.getByLabel('Visibility mode').click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await page
.getByRole('menuitem', {
name: 'Reading',
@@ -246,8 +242,8 @@ test.describe('Doc Visibility: Public', () => {
cardContainer.getByText('Public document', { exact: true }),
).toBeVisible();
await expect(page.getByRole('button', { name: 'search' })).toBeVisible();
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
await expect(page.getByTestId('search-docs-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
const urlDoc = page.url();
@@ -262,8 +258,8 @@ test.describe('Doc Visibility: Public', () => {
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await expect(page.getByRole('button', { name: 'search' })).toBeHidden();
await expect(page.getByRole('button', { name: 'New doc' })).toBeHidden();
await expect(page.getByTestId('search-docs-button')).toBeHidden();
await expect(page.getByTestId('new-doc-button')).toBeHidden();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
const card = page.getByLabel('It is the card information');
await expect(card).toBeVisible();
@@ -293,7 +289,7 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await page
@@ -306,7 +302,7 @@ test.describe('Doc Visibility: Public', () => {
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByLabel('Visibility mode').click();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Editing').click();
await expect(
@@ -362,7 +358,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -414,7 +410,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -441,7 +437,7 @@ test.describe('Doc Visibility: Authenticated', () => {
const { name: childTitle } = await createRootSubPage(
page,
browserName,
'Authenticated read onlyc - child',
'Authenticated read only - child',
);
const urlChildDoc = page.url();
@@ -455,9 +451,7 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible({
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
@@ -501,6 +495,7 @@ test.describe('Doc Visibility: Authenticated', () => {
page,
browserName,
}) => {
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
@@ -514,7 +509,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByLabel('Visibility', { exact: true });
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await page
.getByRole('menuitem', {
@@ -527,7 +522,7 @@ test.describe('Doc Visibility: Authenticated', () => {
).toBeVisible();
const urlDoc = page.url();
await page.getByLabel('Visibility mode').click();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Editing').click();
await expect(
@@ -545,15 +540,17 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = BROWSERS.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs' }),
).toBeVisible();
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
await page.getByRole('button', { name: 'Copy link' }).click();
await expect(page.getByText('Link Copied !')).toBeVisible();
await expect(page.getByText('Link Copied !')).toBeVisible({
timeout: 10000,
});
});
});
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './common';
import { overrideConfig } from './utils-common';
test.describe('Footer', () => {
test.use({ storageState: { cookies: [], origins: [] } });
@@ -21,7 +21,7 @@ test.describe('Footer', () => {
await expect(footer.getByAltText('Docs Logo')).toBeVisible();
await expect(footer.getByRole('heading', { name: 'Docs' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'Github' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'GitHub' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'DINUM' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'ZenDiS' })).toBeVisible();
@@ -1,6 +1,10 @@
import { expect, test } from '@playwright/test';
import { expectLoginPage, keyCloakSignIn, overrideConfig } from './common';
import {
expectLoginPage,
keyCloakSignIn,
overrideConfig,
} from './utils-common';
test.describe('Header', () => {
test('checks all the elements are visible', async ({ page }) => {
@@ -8,8 +12,8 @@ test.describe('Header', () => {
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
await expect(header.getByTestId('header-logo-link')).toBeVisible();
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
'font-family',
/Roboto/i,
);
@@ -33,8 +37,8 @@ test.describe('Header', () => {
const header = page.locator('header').first();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.locator('h1').getByText('Docs')).toHaveCSS(
'font-family',
/Marianne/i,
);
@@ -102,7 +106,7 @@ test.describe('Header mobile', () => {
const header = page.locator('header').first();
await expect(header.getByLabel('Open the header menu')).toBeVisible();
await expect(header.getByRole('link', { name: 'Docs Logo' })).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(
header.getByRole('button', {
name: 'Les services de La Suite numérique',
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './common';
import { overrideConfig } from './utils-common';
test.beforeEach(async ({ page }) => {
await page.goto('/docs/');
@@ -15,10 +15,13 @@ test.describe('Home page', () => {
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
await expect(
header.getByRole('button', { name: /Language/ }),
).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
const languageButton = page.getByRole('button', {
name: /Language|Select language/,
});
await expect(languageButton).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles
@@ -65,20 +68,31 @@ test.describe('Home page', () => {
await page.goto('/docs/');
// Wait for the page to be fully loaded and responsive store to be initialized
await page.waitForLoadState('domcontentloaded');
// Wait a bit more for the responsive store to be initialized
await page.waitForTimeout(500);
// Check header content
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
await expect(
header.getByRole('button', { name: /Language/ }),
).toBeVisible();
// Check for language picker - it should be visible on desktop
// Use a more flexible selector that works with both Header and HomeHeader
const languageButton = page.getByRole('button', {
name: /Language|Select language/,
});
await expect(languageButton).toBeVisible();
await expect(
header.getByRole('button', { name: 'Les services de La Suite numé' }),
).toBeVisible();
await expect(
header.getByRole('img', { name: 'Gouvernement Logo' }),
).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
// Check the titles
@@ -1,30 +1,17 @@
import { Page, expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { createDoc } from './common';
test.describe.serial('Language', () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
test.describe('Language', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
});
test.afterEach(async ({ page }) => {
// Switch back to English - important for other tests to run as expected
await waitForLanguageSwitch(page, TestLanguage.English);
});
test('checks language switching', async ({ page }) => {
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
// initial language should be english
await expect(
@@ -36,17 +23,57 @@ test.describe.serial('Language', () => {
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(
header.getByRole('button').getByText('Français'),
).toBeVisible();
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
await header.getByRole('button').getByText('Français').click();
await page.getByLabel('Deutsch').click();
// Switch to German using the utility function for consistency
await waitForLanguageSwitch(page, TestLanguage.German);
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
await expect(page.getByLabel('Abmelden')).toBeVisible();
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
await languagePicker.click();
await expect(page.locator('[role="menu"]')).toBeVisible();
const menuItems = page.getByRole('menuitem');
await expect(menuItems.first()).toBeVisible();
await menuItems.first().click();
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
await expect(languagePicker).toContainText('English');
});
test('can switch language using only keyboard', async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
const languagePicker = page.getByRole('button', {
name: /select language/i,
});
await expect(languagePicker).toBeVisible();
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
const menu = page.getByRole('menu');
await expect(menu).toBeVisible();
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(page.locator('html')).not.toHaveAttribute('lang', 'en-us');
});
test('checks that backend uses the same language as the frontend', async ({
@@ -94,48 +121,3 @@ test.describe.serial('Language', () => {
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
});
});
// language helper
export const TestLanguage = {
English: {
label: 'English',
expectedLocale: ['en-us'],
},
French: {
label: 'Français',
expectedLocale: ['fr-fr'],
},
German: {
label: 'Deutsch',
expectedLocale: ['de-de'],
},
} as const;
type TestLanguageKey = keyof typeof TestLanguage;
type TestLanguageValue = (typeof TestLanguage)[TestLanguageKey];
export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
) {
const header = page.locator('header').first();
const languagePicker = header.locator('.--docs--language-picker-text');
const isAlreadyTargetLanguage = await languagePicker
.innerText()
.then((text) => text.toLowerCase().includes(lang.label.toLowerCase()));
if (isAlreadyTargetLanguage) {
return;
}
await languagePicker.click();
const responsePromise = page.waitForResponse(
(resp) =>
resp.url().includes('/user') && resp.request().method() === 'PATCH',
);
await page.getByLabel(lang.label).click();
const resolvedResponsePromise = await responsePromise;
const responseData = await resolvedResponsePromise.json();
expect(lang.expectedLocale).toContain(responseData.language);
}
@@ -8,8 +8,8 @@ test.describe('Left panel desktop', () => {
test('checks all the elements are visible', async ({ page }) => {
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
await expect(page.getByRole('button', { name: 'house' })).toBeVisible();
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
await expect(page.getByTestId('home-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
});
});
@@ -27,9 +27,11 @@ test.describe('Left panel mobile', () => {
await expect(page.getByTestId('left-panel-mobile')).not.toBeInViewport();
const header = page.locator('header').first();
const homeButton = page.getByRole('button', { name: 'house' });
const newDocButton = page.getByRole('button', { name: 'New doc' });
const languageButton = page.getByRole('button', { name: /Language/ });
const homeButton = page.getByTestId('home-button');
const newDocButton = page.getByTestId('new-doc-button');
const languageButton = page.getByRole('button', {
name: 'Select language',
});
const logoutButton = page.getByRole('button', { name: 'Logout' });
await expect(homeButton).not.toBeInViewport();
@@ -1,158 +0,0 @@
import { Locator, Page, expect } from '@playwright/test';
export type UserSearchResult = {
email: string;
full_name?: string | null;
};
export type Role = 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader';
export type LinkReach = 'Private' | 'Connected' | 'Public';
export type LinkRole = 'Reading' | 'Edition';
export const searchUserToInviteToDoc = async (
page: Page,
inputFill?: string,
): Promise<UserSearchResult[]> => {
const inputFillValue = inputFill ?? 'user ';
const responsePromise = page.waitForResponse(
(response) =>
response
.url()
.includes(`/users/?q=${encodeURIComponent(inputFillValue)}`) &&
response.status() === 200,
);
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
await expect(inputSearch).toBeVisible();
await inputSearch.fill(inputFillValue);
const response = await responsePromise;
const users = (await response.json()) as UserSearchResult[];
return users;
};
export const addMemberToDoc = async (
page: Page,
role: Role,
users: UserSearchResult[],
) => {
const list = page.getByTestId('doc-share-add-member-list');
await expect(list).toBeHidden();
const quickSearchContent = page.getByTestId('doc-share-quick-search');
for (const user of users) {
await quickSearchContent
.getByTestId(`search-user-row-${user.email}`)
.click();
}
await list.getByLabel('doc-role-dropdown').click();
await expect(page.getByLabel(role)).toBeVisible();
await page.getByLabel(role).click();
await page.getByRole('button', { name: 'Invite' }).click();
};
export const verifyMemberAddedToDoc = async (
page: Page,
user: UserSearchResult,
role: Role,
): Promise<Locator> => {
const container = page.getByLabel('List members card');
await expect(container).toBeVisible();
const userRow = container.getByTestId(`doc-share-member-row-${user.email}`);
await expect(userRow).toBeVisible();
await expect(userRow.getByText(role)).toBeVisible();
await expect(userRow.getByText(user.full_name || user.email)).toBeVisible();
return userRow;
};
export const updateShareLink = async (
page: Page,
linkReach: LinkReach,
linkRole?: LinkRole | null,
) => {
await page.getByRole('button', { name: 'Visibility', exact: true }).click();
await page.getByRole('menuitem', { name: linkReach }).click();
const visibilityUpdatedText = page
.getByText('The document visibility has been updated')
.first();
await expect(visibilityUpdatedText).toBeVisible();
if (linkRole) {
await page
.getByRole('button', { name: 'Visibility mode', exact: true })
.click();
await page.getByRole('menuitem', { name: linkRole }).click();
await expect(visibilityUpdatedText).toBeVisible();
}
};
export const verifyLinkReachIsDisabled = async (
page: Page,
linkReach: LinkReach,
) => {
await page.getByRole('button', { name: 'Visibility', exact: true }).click();
const item = page.getByRole('menuitem', { name: linkReach });
await expect(item).toBeDisabled();
await page.click('body');
};
export const verifyLinkReachIsEnabled = async (
page: Page,
linkReach: LinkReach,
) => {
await page.getByRole('button', { name: 'Visibility', exact: true }).click();
const item = page.getByRole('menuitem', { name: linkReach });
await expect(item).toBeEnabled();
await page.click('body');
};
export const verifyLinkRoleIsDisabled = async (
page: Page,
linkRole: LinkRole,
) => {
await page
.getByRole('button', { name: 'Visibility mode', exact: true })
.click();
const item = page.getByRole('menuitem', { name: linkRole });
await expect(item).toBeDisabled();
await page.click('body');
};
export const verifyLinkRoleIsEnabled = async (
page: Page,
linkRole: LinkRole,
) => {
await page
.getByRole('button', { name: 'Visibility mode', exact: true })
.click();
const item = page.getByRole('menuitem', { name: linkRole });
await expect(item).toBeEnabled();
await page.click('body');
};
export const verifyShareLink = async (
page: Page,
linkReach: LinkReach,
linkRole?: LinkRole | null,
) => {
const visibilityDropdownButton = page.getByRole('button', {
name: 'Visibility',
exact: true,
});
await expect(visibilityDropdownButton).toBeVisible();
await expect(visibilityDropdownButton.getByText(linkReach)).toBeVisible();
if (linkRole) {
const visibilityModeButton = page.getByRole('button', {
name: 'Visibility mode',
exact: true,
});
await expect(visibilityModeButton).toBeVisible();
await expect(page.getByText(linkRole)).toBeVisible();
}
};

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