Compare commits

...

105 Commits

Author SHA1 Message Date
Anthony LC 5e8dc8f6b3 🔖(patch) release 3.8.21
Changed
♻️(backend) increase user short_name field length

Fixed
🐛(frontend) fix duplicate document entries in grid
🐛(frontend) show full nested doc names with ajustable bar
🐛(backend) fix trashbin list
2025-10-23 00:03:39 +02:00
Anthony LC a714d5da4c 🐛(backend) fix trashbin list
Fix listing of deleted documents in trashbin for
users without owner access
2025-10-23 00:03:38 +02:00
Manuel Raynaud 2b5a9e1af8 ♻️(backend) increase user short_name field length
The user's short_name field length was set to 20. This is not enought
and we have some users who cannot register because of that. We changed
this length to a higher one, 100, like the full_name.
2025-10-22 11:44:39 +02:00
Cyril a833fdc7a1 (frontend) add resizable left panel on desktop with persistence
mainlayout and leftpanel updated with resizable panel saved in localstorage

Signed-off-by: Cyril <c.gromoff@gmail.com>

(frontend) show full nested doc names with horizontal scroll support

horizontal overflow enabled and opacity used for sticky actions visibility

Signed-off-by: Cyril <c.gromoff@gmail.com>

(frontend) show full nested doc names with horizontal scroll support

horizontal overflow enabled and opacity used for sticky actions visibility

Signed-off-by: Cyril <c.gromoff@gmail.com>

(frontend) add resizable-panels lib also used in our shared ui kit

needed for adaptable ui consistent with our shared ui kit components

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-21 10:59:24 +02:00
Anthony LC b3cc2bf833 🚨(eslint) add missing rules
We recently upgraded to Eslint v9, it seems that
it is missing some rules that we had previously.
We add them back:
- @typescript-eslint/no-inferrable-types
- @typescript-eslint/no-floating-promises
2025-10-20 21:53:10 +02:00
Anthony LC 18feab10cb (e2e) reduce flakinees
- Because of parallel test execution, some tests
were flaky when using goToGridDoc, the title
changed between the time we got the document list
and the time we clicked on the document.
- Improve addChild function.
2025-10-20 10:17:03 +02:00
Anthony LC 2777488d24 🐛(frontend) fix duplicate document entries in grid
The tests e2e were failing sometimes because
the documents list was containing duplicates.
This was happening when multiple users were
modifying the documents list (creation, update, ...).
We now deduplicate documents by their ID
before displaying them.
2025-10-20 10:17:03 +02:00
Anthony LC a11258f778 🔖(patch) release 3.8.2
Fixed:

- 🐛(service-worker) fix sw registration and page reload
  logic
2025-10-17 15:54:56 +02:00
Anthony LC 33647f124f 🐛(service-worker) fix sw registration and page reload logic
When a new service worker is installed, the page
was reloaded to ensure the new service worker took
control, it is not a big issue in normal browsing mode
because the service worker is only updated once in a
while (every release).
However, in incognito mode, the service worker has to be
re-registered on each new session, which means that
the page was reloading each time the user opened a
new incognito window, creating a bad user experience.
We now take in consideration the case where the
service-worker is installed for the first time, and don't
reload if it is this case.
2025-10-17 15:14:04 +02:00
Anthony LC e339cda5c6 🔖(patch) release 3.8.1
Fixed:
- ️(backend) improve trashbin endpoint performance
- 🐛(backend) manage invitation partial update without email
- (frontend) improve accessibility:
  -  add missing aria-label to add sub-doc button
  for accessibility
  -  add missing aria-label to more options button
  on sub-docs

Removed:
- 🔥(backend) remove treebeard form for the document admin
2025-10-17 10:41:38 +02:00
Manuel Raynaud 4ce65c654f 🔥(backend) remove treebeard form for the document admin
The document change admin page is unusable. The django treebeard library
can change the form used by one provided but this one is really slow.
And it is collapsing the configuration made with the other fields and
readonly fields declared on the DocumentAdmin class. In a first time we
remove the form usage, it seems useless. Later we have to provide more
information on this admin page.
2025-10-17 08:35:22 +00:00
Manuel Raynaud c048b2ae95 🐛(backend) manage invitation partial update without email
An invitation can be updated to change its role. The front use a PATCH
sending only the changed role, so the email is missing in the
InivtationSerializer.validate method. We have to check first if an email
is present before working on it.
2025-10-16 15:26:02 +00:00
Manuel Raynaud 5908afb098 ️(backend) improve trashbin endpoint performance (#1495)
The trashbin endpoint is slow. To filter documents the user has owner
access, we use a subquery to compute the roles and then filter on this
subquery. This is very slow. To improve it, we use the same way to
filter children used in the tree endpoint. First we look for all highest
ancestors the user has access on with the owner role. Then we create one
queryset filtering on all the docs starting by the given path and are
deleted.
2025-10-16 17:06:47 +02:00
Cyril e2298a3658 (frontend) add missing aria-label to more options button on sub-docs
improves accessibility by making the options button screen reader friendly

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-16 15:02:04 +02:00
Cyril 278eb233e9 (frontend) add missing aria-label to add sub-doc button for a11y
improves screen reader support for the add sub-doc action in the document tree

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-16 14:18:55 +02:00
Anthony LC b056dbfad4 🔖(minor) release 3.8.0
Added:
- (frontend) add pdf block to the editor
- List and restore deleted docs

Changed:
- ♻️(frontend) Refactor Auth component for improved redirection logic
- ♻️(frontend) replace Arial font-family with token font
- (frontend) improve accessibility:
  - (frontend) enable enter key to open documentss
  - (frontend) improve modal a11y: structure, labels, title
  - improve NVDA navigation in DocShareModal
  -  improve accessibility by adding landmark roles to layout
  -  add document visible in list and openable via enter key
  -  add pdf outline property to enable bookmarks display
  -  hide decorative icons from assistive tech with aria-hidden
  -  fix rgaa 1.9.1: convert to figure/figcaption structure
  -  remove redundant aria-label to avoid over-accessibility
  -  remove redundant aria-label on hidden icons and update tests
  -  improve semantic structure and aria roles of leftpanel
  -  add default background to left panel for better accessibility
  -  restyle checked checkboxes: removing strikethrough
  -  add h1 for SR on 40X pages and remove alt texts
  -  update labels and shared document icon accessibility
- 🍱(frontend) Fonts GDPR compliants
- ♻️(service-worker) improve SW registration and update handling

Fixed:
- 🐛(backend) duplicate sub docs as root for reader users
- ⚗️(service-worker) remove index from cache first strategy
- 🐛(frontend) fix 404 page when reload 403 page
- 🐛(frontend) fix legacy role computation
- 🛂(frontend) block editing title when not allowed
- 🐛(frontend) scroll back to top when navigate to a document
- 🐛(frontend) fix export pdf emoji problem
- 🐛(frontend) fix attachment download filename
- 🐛(frontend) exclude h4-h6 headings from table of contents
- 🔒(frontend) prevent readers from changing callout emoji
- 🐛(frontend) fix overlapping placeholders in multi-column layout
- 🐛(backend) filter invitation with case insensitive email
- 🐛(frontend) reduce no access image size from 450 to 300
- 🐛(frontend) preserve interlink style on drag-and-drop in editor
- (frontend) load docs logo from public folder via url
- 🔧(keycloak) Fix https required issue in dev mode
2025-10-14 17:12:45 +02:00
AntoLC 771ef2417f 🌐(i18n) update translated strings
Update translated files with new translations
2025-10-14 17:12:45 +02:00
Anthony LC 8d5262c2f2 (e2e) fix flaky tests
Some tests were flaky, we improved their stability.
2025-10-14 16:55:51 +02:00
Anthony LC 1125f441dc 🔇(e2e) remove unnecessary console log
A test had a console log that was not useful.
2025-10-14 14:48:54 +02:00
Anthony LC 16f2de4c75 ♻️(frontend) logo theme customization optional
To not create a breaking change, the logo in the
theme customization is now optional, meaning that
if no logo is provided, the default logo will be used.

We add some documentation about this feature.
2025-10-14 14:48:54 +02:00
Anthony LC f19fa93600 💄(frontend) fix gap and alignment icons
Fix some gaps and alignment of the icons.
2025-10-14 14:48:54 +02:00
Anthony LC af3d90db3b 🐛(frontend) fix double scrollbar on document grid
The document grid was showing a double scrollbar.
It was due to the sr-only class having a width and
height of 1px.
We changed it to 0px, it is now fixed.
2025-10-14 14:48:54 +02:00
Anthony LC 127c90ca5f 🐛(frontend) fix circular dependency problems
A circular dependency was introduced in the previous
commit.
This commit resolves the circular dependency by
refactoring the code to remove the circular reference.
2025-10-14 08:56:12 +02:00
Anthony LC fa7cf7a594 💄(frontend) add a theme focus visible on BoxButton
We want to improve the accessibility of our BoxButton
component by adding a theme focus visible style.
This will help users who navigate using the
keyboard to easily identify which button is currently
focused.
To do so we have to move some theme styles to
the Box component to be able to use them in
BoxButton.
2025-10-14 08:56:12 +02:00
Anthony LC 6523165ea0 (frontend) doc page when deleted
Whe the doc is deleted, the doc page is a bit
different, we have to adapt the doc header
to add some information and actions that
are relevant for a deleted doc.
2025-10-14 08:56:11 +02:00
Anthony LC de4d11732f (frontend) can restore from trashbin list actions
We can now restore a doc from the trashbin list actions.
2025-10-14 08:55:26 +02:00
Anthony LC 37138c1a23 (frontend) add trashbin list
List the docs deleted in the trashbin list,
it is displayed in the docs grid.
2025-10-14 08:52:50 +02:00
Anthony LC 2c1a9ff74f 🍱(frontend) add material-symbols-outlined font
The design uses Material Symbols for icons.
This commit adds the font to the project and
updates the Icon component to be able to use it.
2025-10-14 08:52:50 +02:00
Manuel Raynaud 31389bcae2 ♻️(backend) open tree endpoint to deleted documents only for owners
The tree endpoint will now return a result only for owners. For other
users the endpoint still returns a 403. Also, the endpoint does look for
ancestors anymore, it only stay on the current document.
2025-10-13 11:15:22 +02:00
Manuel Raynaud f772801fd0 ♻️(backend) change abilities for deleted document
The abilities for a deleted document were too open. We want to restrict
them. Only the restore, retrieve and tree is allowed. The tree method
will need some modifications to get the right informations.
2025-10-13 11:15:22 +02:00
Manuel Raynaud 390a615f48 (backend) expose deleted_at information in serializer
The front needs to know when a document has been deleted. We expose the
deleted_at property on a document object,
2025-10-13 11:15:21 +02:00
Anthony LC 5bdf5d2210 🔧(backend) expose TRASHBIN_CUTOFF_DAYS
To know when a document in the trashbin will be
permanently deleted.
2025-10-13 11:15:21 +02:00
Anthony LC ed336558ac 🧑‍💻(config) THEME_CUSTOMIZATION_CACHE_TIMEOUT to 15 seconds
In development mode, we will reduce the cache timeout
for theme customization to 15 seconds.
This change allows developers to see updates
to theme settings more quickly without needing
to clear the cache manually.
2025-10-13 08:16:19 +02:00
Cyril 4fbd588198 (frontend) load docs logo from public folder via url instead of svg
allows logo override at deploy-time using k8s configmaps and static assets

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-13 08:15:57 +02:00
rvveber 546f97c956 ♻️(frontend) Refactor Auth component for improved redirection logic
Move redirects from render
to a guarded useEffect
to avoid triggering multiple redirects
on every re-render.
2025-10-09 21:49:41 +02:00
Anthony LC af01c6e466 ♻️(service-worker) improve SW registration and update handling
It is apparently a bad practice to add the version
number to the service worker file name.
This prevents the browser from properly updating
the service worker when a new version is available.

We improve the update handling by a more usual
pattern.
2025-10-09 21:34:32 +02:00
Anthony LC 8023720da3 🙈(frontend) ignore public/assets/fonts with stylelint
The app build can sometimes fail because stylelint
linter warns about css font files in
public/assets/fonts.
We do not need to lint these files as they are
third-party files.
2025-10-09 16:23:52 +02:00
Cyril 91eba31735 (frontend) preserve interlink style on drag-and-drop in editor
adds hook to normalize dropped blocks and restore internal link format

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-09 13:39:56 +02:00
Manuel Raynaud 45d6c1beef 🔧(action) publish a comment when label preview is added (#1467)
The label preview will deploy a full environment. This environment is
accessible using a specific url. This commit will publish a comment with
the good url.
2025-10-09 12:17:17 +02:00
Tom Kaltenbrunner dc25f3f39c 🚚(docker) use static path for certifi ca certificate
The certifi ca certificate is now stored under a static path
(/cert/cacert.pem) to avoid issues when python is upgraded and the path
to the certificate changes.
2025-10-08 12:50:31 +00:00
Cyril 529e7f1737 (frontend) reduce no access image size from 450 to 300
image now matches mockups (300x300px)

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-08 10:43:13 +02:00
Cyril 51c5c4ee63 (frontend) fix overlapping placeholders in multi-column layout
placeholders no longer overlap when adding 2–3 column, text wraps correctly

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-08 09:37:58 +02:00
Anthony LC 72f098c667 🐛(frontend) email case sensitive search modal
When a user was searching for an email in the share modal,
the search was case sensitive, so we were proposing
to send an invitation to a new user when in
fact the user was already registered.
The search is now case insensitive, so the only
choice is to add the existing user in the share list.
2025-10-07 14:51:46 +02:00
Manuel Raynaud 3b08ba4de1 ♻️(backend) force saving invitation email in lowercase
We want to enforce that invitation email are saved in lower case.
2025-10-07 14:51:46 +02:00
Manuel Raynaud 590b67fd71 🐛(backend) filter invitation with case insensitive email
A user can be invited and no control is made on the email case. Then,
when a new user is created, we are looking if there are pending
invitation and the lookup used is case sensitive. We change it using
__iexact which is case insensitive.
2025-10-07 14:51:45 +02:00
rouja b3980e7bf1 ♻️(ci) add feature app environment
Add a special environment to prepare feature app deployment
2025-10-07 09:42:34 +00:00
Anthony LC e3b2fdbdf5 🔥(mail) replace google font Roboto
Google fonts are not GDPR. We remove them and use
system fonts instead.
2025-10-07 09:06:39 +02:00
Anthony LC 314a7fa7b0 🍱(frontend) manage Inter font locally
We now use @fontsource-variable/inter package
instead of relying on a Google CDN.
2025-10-07 09:06:39 +02:00
Anthony LC 93227466d2 🍱(frontend) manage export emoji locally
We now use exported emoji from emoji-datasource-apple
package instead of relying on a CDN.
During a build or dev command, the emoji images
are copied from node_modules to the public assets
folder. They are not versionned.
2025-10-07 09:06:39 +02:00
Anthony LC db7ae350ec ♻️(frontend) Marianne single source of truth
We now use Marianne font from @gouvfr-lasuite/ui-kit
package instead of a versionned local copies.
This ensures we always use the latest version of
the font.
During a build or dev command, Marianne font files
are copied from node_modules to the public assets
folder. They can stop being versionned.
2025-10-06 18:54:20 +02:00
Anthony LC 236c8df5ae 🚨(eslint) add no-unnecessary-type-assertion lint rule
Add no-unnecessary-type-assertion rule to eslint
config to avoid automatically unnecessary type
assertions in the codebase.
2025-10-06 16:49:58 +02:00
Cyril ae1b05189e (frontend) fix attachment download filename
use the document title instead of the uuid when downloading attachments

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-06 16:49:58 +02:00
Cyril 431c331154 (frontend) exclude h4-h6 headings from table of contents
filters out h4-h6 so they no longer appear in the document outline

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-06 11:28:18 +02:00
Cyril 5184723862 (frontend) prevent readers from changing callout emoji
restrict callout emoji editing to maintain proper permissions

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-06 10:45:10 +02:00
virgile-dev ca10fb9a12 💬(copy) wrong permission mentionned on move doc (#1377)
## Purpose

To move a doc you have to be at least admin of the destination doc.
It was written editor in the alert banner.


## Proposal
Change editor for administrator

## 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: virgile-deville <virgile.deville@beta.gouv.fr>
2025-10-03 14:16:43 +00:00
dependabot[bot] 59e875764c ⬆️(backend) bump django from 5.2.6 to 5.2.7
Upgrade to django 5.2.7
It is a security release with a fix for 2 CVE
https://docs.djangoproject.com/en/5.2/releases/5.2.7/
2025-10-03 08:29:34 +00:00
Anthony LC 7ed46ab225 🛂(frontend) block editing title when not allowed
We had a case where the title input was editable
even when the user did not have the right to
edit it because of websocket problem during
collaboration. We fixed this issue by checking
the collaboration status before allowing the
edition of the title.
2025-10-02 16:07:58 +02:00
Cyril 18f4ab880f (frontend) update labels and shared document icon accessibility
remove aria-labels from decorative icons and add sr-only text to shared doc icon

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-02 13:24:47 +02:00
Cyril e71c45077d (frontend) checked checkboxes: removing strikethrough
removing strikethrough

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-01 11:41:18 +02:00
Cyril 14c84f000e (frontend) add h1 for SR on 40X pages and remove alt texts
improves screen reader support on error pages by clarifying structure

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-30 08:57:30 +02:00
Cyril 6cc42636e5 (frontend) convert to figure/figcaption structure if caption exists
ensure  html structure by using figure/figcaption when captions are present

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-29 10:42:56 +02:00
Anthony LC cc4bed6f8e ♻️(frontend) add upload loader block
The way we were handling the antivirus upload loader
was not optimal, it didn't work well with the pdf
embed block. We created a dedicated upload loader
block, it will replace the previous implementation,
it is more Blocknote idiomatic and will work
better with any type of upload files.
2025-09-26 17:15:22 +02:00
dakshesh14 d8f90c04bd (frontend) add pdf blocks to the editor
Added pdf block in the editor.

Signed-off-by: dakshesh14 <65905942+dakshesh14@users.noreply.github.com>
2025-09-26 17:15:22 +02:00
Cyril 1fdf70bdcf (frontend) remove redundant aria-label on hidden icons and update tests
remove aria-label from aria-hidden icons and update tests with data-testid

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-24 13:19:08 +02:00
Cyril 8ab21ef00d (frontend) improve semantic structure and aria roles of leftpanel
use nav and appropriate aria attributes to enhance accessibility

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-24 12:19:47 +02:00
Cyril f337a2a8f2 (frontend) add default background to left panel for better a11y
defined a default background color to prevent issues with user stylesheets

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-24 11:40:49 +02:00
Cyril 3607faa475 (frontend) remove redundant aria-label to avoid over-accessibility
aria-label was removed because the visible span already provides the text

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-23 11:26:28 +02:00
Manuel Raynaud 0ea7dd727f 🔒️(frontend) update alpine packages in production image (#1425)
Force an update of installed package in the image used for the frontend
in production.
2025-09-23 09:21:01 +00:00
Anthony LC 6aca40a034 ⬆️(dependencies) Bump vite from 7.1.0 to 7.1.5
Bumps vite from 7.1.0 to 7.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md
- https://github.com/vitejs/vite/commits/v7.1.5/packages/vite

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-22 15:56:23 +02:00
Cyril ee3b05cb55 (frontend) improve NVDA navigation in DocShareModal
fix NVDA focus and announcement issues in search modal combobox

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-22 14:53:34 +02:00
Anthony LC c23ff546d8 🐛(frontend) scroll back to top when navigate to a document
When navigating to a new document, the scroll
position was preserved. This commit changes this
behavior to scroll back to the top of
the page when navigating to a new document.
2025-09-22 10:52:34 +02:00
Anthony LC a751f1255a ♻️(frontend) replace Arial font-family with token font
In some components, the Arial font was still used
because of a centering problem.
We removed all instances of Arial and replaced them
with the current font token, the centering problems
were fixed by adding "contain: content;" to the css.
2025-09-22 10:09:15 +02:00
Anthony LC 8ee50631f3 🍱(frontend) replace Marianne font
Some improvements has been made to the Marianne
fonts. We replace the previous one with the
newer version.
2025-09-22 10:09:14 +02:00
Cyril e5e5fba0b3 (frontend) hide decorative icons from assistive tech with aria-hidden
improves accessibility by reducing screen reader noise from icon elements

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-22 08:31:51 +02:00
Cyril 0894bcdca5 (docs) add title metadata to exported docx/pdf for accessibility
ensures document title is preserved in exports to meet accessibility needs

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-18 14:55:02 +02:00
Anthony LC 75da342058 🏷️(frontend) adapt types to link-configuration endpoint
The link-configuration endpoint has now a strict
validation schema about the combination of
link_reach and link_role.
We need to adapt our types
frontend side to reflect that.
2025-09-18 13:16:37 +02:00
Anthony LC 1ed01fd64b 🥅(backend) link role could be updated when restricted document
When a document was restricted, the link role could
be updated from "link-configuration" and gives a
200 response, but the change did not
have any effect because of a restriction in
LinkReachChoices.
We added a validation step to ensure that the
link role can only be updated if the document
is not restricted.
2025-09-18 12:17:08 +02:00
Anthony LC e4aa85be83 (e2e) fix flakiness
Some tests were getting very flaky due to previous
tests updates. This should fix it.
2025-09-18 11:28:20 +02:00
Anthony LC 2dc1e07b42 ⚗️(service-worker) remove index from cache first strategy
Some users reported that the app was giving a
blank page, it seems to happens often after a
release. It could be due to the fact that
the service worker is caching the index.html
file and not updating it properly after a new release.
We remove the index from the cache first strategy
to avoid this kind of issue. We set as well
the default handler with the "StaleWhileRevalidate"
strategy to force the cache to be updated in
the background.
2025-09-18 10:40:47 +02:00
Anthony LC fbdeb90113 🛂(frontend) invalidate doc query when lost connection
When the provider reports a lost connection,
we invalidate the doc query to refetch the document
data.
This ensures that if a user has lost is rights
to access a document, he will be redirected
to a 403 page without needing to refresh the page.
2025-09-17 17:45:26 +02:00
Anthony LC b773f09792 🥅(frontend) improve meta 401 page
Add better meta for the 401 page.
2025-09-17 17:45:26 +02:00
Anthony LC d8c9283dd1 🐛(frontend) fix 404 page when reload 403 page
When users were reloading a 403 page, they were
redirected to the 404 page because of Nextjs
routing mechanism. This commit fixes this issue by
removing the 403 page from the pages directory
and creating a component that is used directly
in the layout when a 403 error is detected.
2025-09-17 17:45:26 +02:00
Cyril 1e39d17914 (frontend) improve accessibility by adding landmark roles to layout
landmark roles help assistive tech users navigate quickly across the page

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-17 08:24:21 +02:00
Anthony LC ecd2f97cf5 🐛(frontend) fix legacy role computation
Before the subpages feature, the user_role was
computed thanks to the abilities.
This is not the correct way to do it anymore,
the abilities are now different.
We now have "user_role" in the doc response
which is the correct way to get the user role
for the current document.
2025-09-16 17:23:26 +02:00
Anthony LC 90624e83f5 🩹(demo) update the email in realm.json
We updated the email addresses for the demo users
but forgot to change them in the realm.json file.
This commit fixes that oversight.
2025-09-16 17:23:26 +02:00
Cyril 5fc002658c (frontend) add pdf outline property to enable bookmarks display
allows pdf viewers like adobe reader to display bookmarks in the sidebar

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-16 14:29:53 +02:00
Cyril dfd5dc1545 (frontend) document visible in list are now openable via enter key
the document now appears in the list and can be opened using the enter key

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-16 12:51:23 +02:00
Cyril 69e7235f75 (frontend) refine focus outline with shadow for visual consistency
aligns focus state with app style by adding background shadow to outline

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-16 10:56:34 +02:00
Cyril 942c90c29f (frontend) enable enter key to open documents and subdocuments
added keyboard support to open docs and subdocs using the enter key

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-16 10:26:49 +02:00
virgile-dev c5f0142671 📝 (doc) add mosa.cloud docs instance (#1334)
## Purpose

So that users have more options to choose from


## Proposal
Add mosa.cloud docs instance url

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-deville <virgile.deville@beta.gouv.fr>
2025-09-16 07:01:10 +00:00
Manuel Raynaud 7f37d3bda4 🐛(backend) duplicate sub docs as root for reader user
Reader user should be able to duplicate a doc in the doc tree. It should
be created a new doc at the root level.
2025-09-15 20:44:58 +00:00
Manuel Raynaud 7033d0ecf7 🐛(backend) cast DOCUMENT_IMAGE_MAX_SIZE in integer
The expected type for the settings DOCUMENT_IMAGE_MAX_SIZE is an
integer. By not using django configurations IntegerValue, the value is
used as it and most of the time will be a string. We must use the
IntegerValue in order to cast the value in string.
2025-09-15 17:47:43 +02:00
Fabre Florian 0dd6818e91 (frontend) Adapt e2e test utils to the Keycloak 26.3 login page
Fix the keyCloakSignIn() function for the new login page.

Signed-off-by: Fabre Florian <ffabre@hybird.org>
2025-09-15 11:19:42 +02:00
Fabre Florian eb225fc86f 🔧(keycloak) Fix https required issue in dev mode
On some environments keycloak returns a 'HTTPS required' message on login.
The same issue was fixed in drive by changing the 'sslRequired' value
from 'external' to 'none'.
Also upgrade keycloak up to 26.3.2

Signed-off-by: Fabre Florian <ffabre@hybird.org>
2025-09-15 11:19:41 +02:00
Anthony LC b893a29138 🔖(minor) release 3.7.0
Added:
- (api) add API route to fetch document content

Changed:
- 🔒️(backend) configure throttle on every viewsets
- ⬆️ Bump eslint to V9
- (frontend) improve accessibility:
  - fix major accessibility issues reported
  by wave and axe
  - unify tab focus style for better visual consistency
  - improve modal a11y: structure, labels, and title
  - improve accessibility of cdoc content with
  correct aria tags
  - unify tab focus style for better visual consistency
  - hide decorative icons, label menus, avoid
  accessible name
- ♻️(tilt) use helm dev-backend chart

Removed:
- 🔥(frontend) remove multi column drop cursor

Fixed:
- 🐛(frontend) fix callout emoji list
2025-09-12 14:21:13 +02:00
Anthony LC a812580d6c ♻️(frontend) add categories on top of the EmojiPicker
In a recent fix we had to remove the categories
from the EmojiPicker component due to a bug in the
underlying library. This commit reintroduces the
categories feature, placing them at the top of the
picker for improved user experience. The
categories help users quickly find emojis
by grouping them into relevant sections.

We set the default color as well to ensure
consistency across the emoji picker.
2025-09-12 14:21:13 +02:00
AntoLC 1062e38c92 🌐(i18n) update translated strings
Update translated files with new translations
2025-09-12 12:11:02 +02:00
renovate[bot] 62e122b05f ⬆️(dependencies) update js dependencies 2025-09-12 11:33:17 +02:00
Anthony LC 32bc2890e0 📌(dependencies) pin wrap-ansi to 9.0.2
By security we pin wrap-ansi to 9.0.2,
the 9.0.1 version being infected.
2025-09-12 10:32:40 +02:00
Anthony LC 3c3686dc7e 🔧(frontend) add meta information to package.json files
- Add missing repository, author, and license fields
- Add recommended packageManager
2025-09-12 10:20:01 +02:00
Anthony LC ab90611c36 🔥(frontend) remove multi column drop cursor
The drop cursor for multi column was causing
issues with the editor's usability.
This commit removes the custom drop cursor
implementation to enhance user experience.
2025-09-11 16:11:48 +02:00
Cyril f9c08cf5ec Revert "(frontend) add document visible in list and openable via enter key"
This reverts commit b619850b1420421f09f56aa8644a93e0fa698682.
2025-09-11 13:43:36 +02:00
Cyril 2155c2ff1f (frontend) add document visible in list and openable via enter key
the document now appears in the list and can be opened using the enter key

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-11 13:43:35 +02:00
Cyril ef08ba3a00 (frontend) hide decorative icons, label menus, avoid name duplicates
improves a11y by hiding decorative icons, labeling menus and deduping names

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-09-11 13:43:20 +02:00
Anthony LC 7a903041f8 🐛(frontend) fix callout emoji list
Removing explicit categories prop from
EmojiPicker to let emoji-mart manage categories
and avoid mismatch causing runtime error with
locales.
2025-09-11 11:54:52 +02:00
228 changed files with 6404 additions and 2865 deletions
+7 -7
View File
@@ -31,7 +31,7 @@ jobs:
images: lasuite/impress-backend
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
@@ -49,7 +49,7 @@ jobs:
context: .
target: backend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -67,7 +67,7 @@ jobs:
images: lasuite/impress-frontend
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USER }}
@@ -88,7 +88,7 @@ jobs:
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
PUBLISH_AS_MIT=false
push: ${{ github.event_name != 'pull_request' }}
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -106,7 +106,7 @@ jobs:
images: lasuite/impress-y-provider
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
@@ -122,7 +122,7 @@ jobs:
file: ./src/frontend/servers/y-provider/Dockerfile
target: y-provider
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
@@ -131,7 +131,7 @@ jobs:
- build-and-push-frontend
- build-and-push-backend
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
steps:
- uses: numerique-gouv/action-argocd-webhook-notification@main
id: notify
+1
View File
@@ -79,6 +79,7 @@ jobs:
--check-filenames \
--ignore-words-list "Dokument,afterAll,excpt,statics" \
--skip "./git/" \
--skip "**/*.pdf" \
--skip "**/*.po" \
--skip "**/*.pot" \
--skip "**/*.json" \
+27
View File
@@ -0,0 +1,27 @@
name: Label Preview
on:
pull_request:
types: [labeled, opened]
permissions:
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'preview')
steps:
- uses: thollander/actions-comment-pull-request@v3
with:
message: |
:rocket: Preview will be available at [https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/](https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/)
You can use the existing account with these credentials:
- username: `docs`
- password: `docs`
You can also create a new account if you want to.
Once this Pull Request is merged, the preview will be destroyed.
comment-tag: preview-url
+3
View File
@@ -75,3 +75,6 @@ db.sqlite3
.vscode/
*.iml
.devcontainer
# Cursor rules
.cursorrules
+104 -9
View File
@@ -1,5 +1,3 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
@@ -8,23 +6,116 @@ and this project adheres to
## [Unreleased]
### Changed
- ♻️(backend) increase user short_name field length
### Fixed
- 🐛(frontend) fix duplicate document entries in grid #1479
- 🐛(frontend) show full nested doc names with ajustable bar #1456
- 🐛(backend) fix trashbin list
## [3.8.2] - 2025-10-17
### Fixed
- 🐛(service-worker) fix sw registration and page reload logic #1500
## [3.8.1] - 2025-10-17
### Fixed
- ⚡️(backend) improve trashbin endpoint performance #1495
- 🐛(backend) manage invitation partial update without email #1494
- ♿(frontend) improve accessibility:
- ♿ add missing aria-label to add sub-doc button for accessibility #1480
- ♿ add missing aria-label to more options button on sub-docs #1481
### Removed
- 🔥(backend) remove treebeard form for the document admin #1470
## [3.8.0] - 2025-10-14
### Added
- ✨(frontend) add pdf block to the editor #1293
- ✨List and restore deleted docs #1450
### Fixed
- 🐛(frontend) show full nested doc names with ajustable bar #1456
### Changed
- ♻️(frontend) Refactor Auth component for improved redirection logic #1461
- ♻️(frontend) replace Arial font-family with token font #1411
- ♿(frontend) improve accessibility:
- ♿(frontend) enable enter key to open documentss #1354
- ♿(frontend) improve modal a11y: structure, labels, title #1349
- ♿improve NVDA navigation in DocShareModal #1396
- ♿ improve accessibility by adding landmark roles to layout #1394
- ♿ add document visible in list and openable via enter key #1365
- ♿ add pdf outline property to enable bookmarks display #1368
- ♿ hide decorative icons from assistive tech with aria-hidden #1404
- ♿ fix rgaa 1.9.1: convert to figure/figcaption structure #1426
- ♿ remove redundant aria-label to avoid over-accessibility #1420
- ♿ remove redundant aria-label on hidden icons and update tests #1432
- ♿ improve semantic structure and aria roles of leftpanel #1431
- ♿ add default background to left panel for better accessibility #1423
- ♿ restyle checked checkboxes: removing strikethrough #1439
- ♿ add h1 for SR on 40X pages and remove alt texts #1438
- ♿ update labels and shared document icon accessibility #1442
- 🍱(frontend) Fonts GDPR compliants #1453
- ♻️(service-worker) improve SW registration and update handling #1473
### Fixed
- 🐛(backend) duplicate sub docs as root for reader users #1385
- ⚗️(service-worker) remove index from cache first strategy #1395
- 🐛(frontend) fix 404 page when reload 403 page #1402
- 🐛(frontend) fix legacy role computation #1376
- 🛂(frontend) block editing title when not allowed #1412
- 🐛(frontend) scroll back to top when navigate to a document #1406
- 🐛(frontend) fix export pdf emoji problem #1453
- 🐛(frontend) fix attachment download filename #1447
- 🐛(frontend) exclude h4-h6 headings from table of contents #1441
- 🔒(frontend) prevent readers from changing callout emoji #1449
- 🐛(frontend) fix overlapping placeholders in multi-column layout #1455
- 🐛(backend) filter invitation with case insensitive email #1457
- 🐛(frontend) reduce no access image size from 450 to 300 #1463
- 🐛(frontend) preserve interlink style on drag-and-drop in editor #1460
- ✨(frontend) load docs logo from public folder via url #1462
- 🔧(keycloak) Fix https required issue in dev mode #1286
## [3.7.0] - 2025-09-12
### Added
- ✨(api) add API route to fetch document content #1206
- ♿(frontend) improve accessibility:
- #1349
- #1271
- #1341
### Changed
- 🔒️(backend) configure throttle on every viewsets #1343
- ⬆️ Bump eslint to V9 #1071
- ♿(frontend) improve accessibility:
-(frontend) fix major accessibility issues reported by wave and axe #1344
- #1341
- ♿fix major accessibility issues reported by wave and axe #1344
- ✨unify tab focus style for better visual consistency #1341
- ✨improve modal a11y: structure, labels, and title #1349
- ✨improve accessibility of cdoc content with correct aria tags #1271
- ✨unify tab focus style for better visual consistency #1341
- ♿hide decorative icons, label menus, avoid accessible name… #1362
- ♻️(tilt) use helm dev-backend chart
### Removed
- 🔥(frontend) remove multi column drop cursor #1370
### Fixed
- 🐛(frontend) fix callout emoji list #1366
## [3.6.0] - 2025-09-04
### Added
@@ -731,7 +822,11 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.6.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.8.2...main
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
[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
+8
View File
@@ -94,6 +94,14 @@ RUN chmod g=u /etc/passwd
# Copy installed python dependencies
COPY --from=back-builder /install /usr/local
# Link certifi certificate from a static path /cert/cacert.pem to avoid issues
# when python is upgraded and the path to the certificate changes.
# The space between print and the ( is intended otherwise the git lint is failing
RUN mkdir /cert && \
path=`python -c 'import certifi;print (certifi.where())'` && \
mv $path /cert/ && \
ln -s /cert/cacert.pem $path
# Copy impress application (see .dockerignore)
COPY ./src/backend /app/
+9 -9
View File
@@ -54,16 +54,16 @@ Docs is a collaborative text editor designed to address common challenges in kno
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.
#### 🌍 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🙏
| | | |
| --- | --- | ------- |
We hope to see many more, here is an incomplete list of public Docs instances. Feel free to make a PR to add ones that are not listed below🙏
| 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/). |
| --- | --- | ------- |
| [docs.numerique.gouv.fr](https://docs.numerique.gouv.fr/) | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up|
| [docs.suite.anct.gouv.fr](https://docs.suite.anct.gouv.fr/) | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up|
| [notes.demo.opendesk.eu](https://notes.demo.opendesk.eu) | ZenDiS | Demo instance of OpenDesk. Request access to get credentials |
| [notes.liiib.re](https://notes.liiib.re/) | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
| [docs.federated.nexus](https://docs.federated.nexus/) | federated.nexus | Public instance, but you have to [sign up for a Federated Nexus account](https://federated.nexus/register/). |
| [docs.demo.mosacloud.eu](https://docs.demo.mosacloud.eu/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
#### ⚠️ 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.
+4 -6
View File
@@ -184,22 +184,20 @@ services:
- env.d/development/kc_postgresql.local
keycloak:
image: quay.io/keycloak/keycloak:20.0.1
image: quay.io/keycloak/keycloak:26.3
volumes:
- ./docker/auth/realm.json:/opt/keycloak/data/import/realm.json
command:
- start-dev
- --features=preview
- --import-realm
- --proxy=edge
- --hostname-url=http://localhost:8083
- --hostname-admin-url=http://localhost:8083/
- --hostname=http://localhost:8083
- --hostname-strict=false
- --hostname-strict-https=false
- --health-enabled=true
- --metrics-enabled=true
healthcheck:
test: ["CMD", "curl", "--head", "-fsS", "http://localhost:8080/health/ready"]
test: ['CMD-SHELL', 'exec 3<>/dev/tcp/localhost/9000; echo -e "GET /health/live HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" >&3; grep "HTTP/1.1 200 OK" <&3']
start_period: 5s
interval: 1s
timeout: 2s
retries: 300
+5 -5
View File
@@ -26,7 +26,7 @@
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"enabled": true,
"sslRequired": "external",
"sslRequired": "none",
"registrationAllowed": true,
"registrationEmailAsUsername": false,
"rememberMe": true,
@@ -60,7 +60,7 @@
},
{
"username": "user-e2e-chromium",
"email": "user@chromium.test",
"email": "user.test@chromium.test",
"firstName": "E2E",
"lastName": "Chromium",
"enabled": true,
@@ -74,7 +74,7 @@
},
{
"username": "user-e2e-webkit",
"email": "user@webkit.test",
"email": "user.test@webkit.test",
"firstName": "E2E",
"lastName": "Webkit",
"enabled": true,
@@ -88,7 +88,7 @@
},
{
"username": "user-e2e-firefox",
"email": "user@firefox.test",
"email": "user.test@firefox.test",
"firstName": "E2E",
"lastName": "Firefox",
"enabled": true,
@@ -2270,7 +2270,7 @@
"cibaInterval": "5",
"realmReusableOtpCode": "false"
},
"keycloakVersion": "20.0.1",
"keycloakVersion": "26.3.2",
"userManagedAccessAllowed": false,
"clientProfiles": {
"profiles": []
+1 -1
View File
@@ -82,7 +82,7 @@ backend:
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
mountPath: /cert/cacert.pem
subPath: cacert.pem
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
+18
View File
@@ -32,6 +32,24 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
----
# **Your logo** 📝
You can add your own logo in the header from the theme customization file.
### Settings 🔧
```shellscript
THEME_CUSTOMIZATION_FILE_PATH=<path>
```
### Example of JSON
You can activate it with the `header.logo` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
This configuration is optional. If not set, the default logo will be used.
----
# **Footer Configuration** 📝
The footer is configurable from the theme customization file.
+3
View File
@@ -66,3 +66,6 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
Y_PROVIDER_API_KEY=yprovider-api-key
# Theme customization
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
-2
View File
@@ -5,7 +5,6 @@ from django.contrib.auth import admin as auth_admin
from django.utils.translation import gettext_lazy as _
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from . import models
@@ -157,7 +156,6 @@ class DocumentAdmin(TreeAdmin):
},
),
)
form = movenodeform_factory(models.Document)
inlines = (DocumentAccessInline,)
list_display = (
"id",
+68
View File
@@ -90,6 +90,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
nb_accesses_direct = serializers.IntegerField(read_only=True)
user_role = serializers.SerializerMethodField(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
deleted_at = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.Document
@@ -102,6 +103,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -124,6 +126,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -165,6 +168,10 @@ class ListDocumentSerializer(serializers.ModelSerializer):
request = self.context.get("request")
return instance.get_role(request.user) if request else None
def get_deleted_at(self, instance):
"""Return the deleted_at of the current document."""
return instance.ancestors_deleted_at
class DocumentLightSerializer(serializers.ModelSerializer):
"""Minial document serializer for nesting in document accesses."""
@@ -193,6 +200,7 @@ class DocumentSerializer(ListDocumentSerializer):
"content",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -216,6 +224,7 @@ class DocumentSerializer(ListDocumentSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"is_favorite",
"link_role",
@@ -506,6 +515,10 @@ class LinkDocumentSerializer(serializers.ModelSerializer):
We expose it separately from document in order to simplify and secure access control.
"""
link_reach = serializers.ChoiceField(
choices=models.LinkReachChoices.choices, required=True
)
class Meta:
model = models.Document
fields = [
@@ -513,6 +526,58 @@ class LinkDocumentSerializer(serializers.ModelSerializer):
"link_reach",
]
def validate(self, attrs):
"""Validate that link_role and link_reach are compatible using get_select_options."""
link_reach = attrs.get("link_reach")
link_role = attrs.get("link_role")
if not link_reach:
raise serializers.ValidationError(
{"link_reach": _("This field is required.")}
)
# Get available options based on ancestors' link definition
available_options = models.LinkReachChoices.get_select_options(
**self.instance.ancestors_link_definition
)
# Validate link_reach is allowed
if link_reach not in available_options:
msg = _(
"Link reach '%(link_reach)s' is not allowed based on parent document configuration."
)
raise serializers.ValidationError(
{"link_reach": msg % {"link_reach": link_reach}}
)
# Validate link_role is compatible with link_reach
allowed_roles = available_options[link_reach]
# Restricted reach: link_role must be None
if link_reach == models.LinkReachChoices.RESTRICTED:
if link_role is not None:
raise serializers.ValidationError(
{
"link_role": (
"Cannot set link_role when link_reach is 'restricted'. "
"Link role must be null for restricted reach."
)
}
)
return attrs
# Non-restricted: link_role must be in allowed roles
if link_role not in allowed_roles:
allowed_roles_str = ", ".join(allowed_roles) if allowed_roles else "none"
raise serializers.ValidationError(
{
"link_role": (
f"Link role '{link_role}' is not allowed for link reach '{link_reach}'. "
f"Allowed roles: {allowed_roles_str}"
)
}
)
return attrs
class DocumentDuplicationSerializer(serializers.Serializer):
"""
@@ -684,6 +749,9 @@ class InvitationSerializer(serializers.ModelSerializer):
if self.instance is None:
attrs["issuer"] = user
if attrs.get("email"):
attrs["email"] = attrs["email"].lower()
return attrs
def validate_role(self, role):
+95 -33
View File
@@ -623,12 +623,32 @@ class DocumentViewSet(
The selected documents are those deleted within the cutoff period defined in the
settings (see TRASHBIN_CUTOFF_DAYS), before they are considered permanently deleted.
"""
if not request.user.is_authenticated:
return self.get_response_for_queryset(self.queryset.none())
access_documents_paths = (
models.DocumentAccess.objects.select_related("document")
.filter(
db.Q(user=self.request.user) | db.Q(team__in=self.request.user.teams),
role=models.RoleChoices.OWNER,
)
.values_list("document__path", flat=True)
)
if not access_documents_paths:
return self.get_response_for_queryset(self.queryset.none())
children_clause = db.Q()
for path in access_documents_paths:
children_clause |= db.Q(path__startswith=path)
queryset = self.queryset.filter(
children_clause,
deleted_at__isnull=False,
deleted_at__gte=models.get_trashbin_cutoff(),
)
queryset = queryset.annotate_user_roles(self.request.user)
queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER])
return self.get_response_for_queryset(queryset)
@@ -851,33 +871,47 @@ class DocumentViewSet(
try:
current_document = (
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
self.queryset.select_related(None)
.only("depth", "path", "ancestors_deleted_at")
.get(pk=pk)
)
except models.Document.DoesNotExist as excpt:
raise drf.exceptions.NotFound() from excpt
ancestors = (
(
current_document.get_ancestors()
| self.queryset.select_related(None).filter(pk=pk)
)
.filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
is_deleted = current_document.ancestors_deleted_at is not None
# Get the highest readable ancestor
highest_readable = (
ancestors.select_related(None)
.readable_per_se(request.user)
.only("depth", "path")
.first()
)
if highest_readable is None:
raise (
drf.exceptions.PermissionDenied()
if request.user.is_authenticated
else drf.exceptions.NotAuthenticated()
if is_deleted:
if current_document.get_role(user) != models.RoleChoices.OWNER:
raise (
drf.exceptions.PermissionDenied()
if request.user.is_authenticated
else drf.exceptions.NotAuthenticated()
)
highest_readable = current_document
ancestors = self.queryset.select_related(None).filter(pk=pk)
else:
ancestors = (
(
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.select_related(None)
.readable_per_se(request.user)
.only("depth", "path")
.first()
)
if highest_readable is None:
raise (
drf.exceptions.PermissionDenied()
if request.user.is_authenticated
else drf.exceptions.NotAuthenticated()
)
paths_links_mapping = {}
ancestors_links = []
children_clause = db.Q()
@@ -941,37 +975,64 @@ class DocumentViewSet(
in the payload.
"""
# Get document while checking permissions
document = self.get_object()
document_to_duplicate = self.get_object()
serializer = serializers.DocumentDuplicationSerializer(
data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
with_accesses = serializer.validated_data.get("with_accesses", False)
is_owner_or_admin = document.get_role(request.user) in models.PRIVILEGED_ROLES
user_role = document_to_duplicate.get_role(request.user)
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
base64_yjs_content = document.content
base64_yjs_content = document_to_duplicate.content
# Duplicate the document instance
link_kwargs = (
{"link_reach": document.link_reach, "link_role": document.link_role}
{
"link_reach": document_to_duplicate.link_reach,
"link_role": document_to_duplicate.link_role,
}
if with_accesses
else {}
)
extracted_attachments = set(extract_attachments(document.content))
attachments = list(extracted_attachments & set(document.attachments))
duplicated_document = document.add_sibling(
extracted_attachments = set(extract_attachments(document_to_duplicate.content))
attachments = list(
extracted_attachments & set(document_to_duplicate.attachments)
)
title = capfirst(_("copy of {title}").format(title=document_to_duplicate.title))
if not document_to_duplicate.is_root() and choices.RoleChoices.get_priority(
user_role
) < choices.RoleChoices.get_priority(models.RoleChoices.EDITOR):
duplicated_document = models.Document.add_root(
creator=self.request.user,
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document_to_duplicate,
**link_kwargs,
)
models.DocumentAccess.objects.create(
document=duplicated_document,
user=self.request.user,
role=models.RoleChoices.OWNER,
)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
)
duplicated_document = document_to_duplicate.add_sibling(
"right",
title=capfirst(_("copy of {title}").format(title=document.title)),
title=title,
content=base64_yjs_content,
attachments=attachments,
duplicated_from=document,
duplicated_from=document_to_duplicate,
creator=request.user,
**link_kwargs,
)
# Always add the logged-in user as OWNER for root documents
if document.is_root():
if document_to_duplicate.is_root():
accesses_to_create = [
models.DocumentAccess(
document=duplicated_document,
@@ -983,7 +1044,7 @@ class DocumentViewSet(
# 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
document=document_to_duplicate
).exclude(user=request.user)
accesses_to_create.extend(
@@ -2130,6 +2191,7 @@ class ConfigView(drf.views.APIView):
"LANGUAGES",
"LANGUAGE_CODE",
"SENTRY_DSN",
"TRASHBIN_CUTOFF_DAYS",
]
dict_settings = {}
for setting in array_settings:
@@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-10-22 06:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0024_add_is_masked_field_to_link_trace"),
]
operations = [
migrations.AlterField(
model_name="user",
name="short_name",
field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="short name"
),
),
]
+11 -8
View File
@@ -148,7 +148,9 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
)
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
short_name = models.CharField(_("short name"), max_length=20, null=True, blank=True)
short_name = models.CharField(
_("short name"), max_length=100, null=True, blank=True
)
email = models.EmailField(_("identity email address"), blank=True, null=True)
@@ -221,7 +223,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
Expired invitations are ignored.
"""
valid_invitations = Invitation.objects.filter(
email=self.email,
email__iexact=self.email,
created_at__gte=(
timezone.now()
- timedelta(seconds=settings.INVITATION_VALIDITY_DURATION)
@@ -721,7 +723,7 @@ class Document(MP_Node, BaseModel):
# Characteristics that are based only on specific access
is_owner = role == RoleChoices.OWNER
is_deleted = self.ancestors_deleted_at and not is_owner
is_deleted = self.ancestors_deleted_at
is_owner_or_admin = (is_owner or role == RoleChoices.ADMIN) and not is_deleted
# Compute access roles before adding link roles because we don't
@@ -750,6 +752,7 @@ class Document(MP_Node, BaseModel):
role = RoleChoices.max(role, link_definition["link_role"])
can_get = bool(role) and not is_deleted
retrieve = can_get or is_owner
can_update = (
is_owner_or_admin or role == RoleChoices.EDITOR
) and not is_deleted
@@ -758,7 +761,7 @@ class Document(MP_Node, BaseModel):
is_owner
if self.is_root()
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
)
) and not is_deleted
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any(
@@ -790,15 +793,15 @@ class Document(MP_Node, BaseModel):
"duplicate": can_get and user.is_authenticated,
"favorite": can_get and user.is_authenticated,
"link_configuration": is_owner_or_admin,
"invite_owner": is_owner,
"invite_owner": is_owner and not is_deleted,
"mask": can_get and user.is_authenticated,
"move": is_owner_or_admin and not self.ancestors_deleted_at,
"move": is_owner_or_admin and not is_deleted,
"partial_update": can_update,
"restore": is_owner,
"retrieve": can_get,
"retrieve": retrieve,
"media_auth": can_get,
"link_select_options": link_select_options,
"tree": can_get,
"tree": retrieve,
"update": can_update,
"versions_destroy": is_owner_or_admin,
"versions_list": has_access_role,
@@ -596,6 +596,32 @@ def test_api_document_invitations_create_cannot_invite_existing_users():
}
def test_api_document_invitations_create_lower_email():
"""
No matter the case, the email should be converted to lowercase.
"""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, "owner")])
# Build an invitation to the email of an existing identity in the db
invitation_values = {
"email": "GuEst@example.com",
"role": random.choice(models.RoleChoices.values),
}
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/documents/{document.id!s}/invitations/",
invitation_values,
format="json",
)
assert response.status_code == 201
assert response.json()["email"] == "guest@example.com"
# Update
@@ -743,6 +769,37 @@ def test_api_document_invitations_update_authenticated_unprivileged(
assert value == old_invitation_values[key]
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize("role", ["administrator", "owner"])
def test_api_document_invitations_patch(via, role, mock_user_teams):
"""Partially updating an invitation should be allowed."""
user = factories.UserFactory()
invitation = factories.InvitationFactory(role="editor")
if via == USER:
factories.UserDocumentAccessFactory(
document=invitation.document, user=user, role=role
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=invitation.document, team="lasuite", role=role
)
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/documents/{invitation.document.id!s}/invitations/{invitation.id!s}/",
{"role": "reader"},
format="json",
)
assert response.status_code == 200
invitation.refresh_from_db()
assert invitation.role == "reader"
# Delete
@@ -41,6 +41,7 @@ def test_api_documents_children_list_anonymous_public_standalone(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -63,6 +64,7 @@ def test_api_documents_children_list_anonymous_public_standalone(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -115,6 +117,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -137,6 +140,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -208,6 +212,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -230,6 +235,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -287,6 +293,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -309,6 +316,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -393,6 +401,7 @@ def test_api_documents_children_list_authenticated_related_direct(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -415,6 +424,7 @@ def test_api_documents_children_list_authenticated_related_direct(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -475,6 +485,7 @@ def test_api_documents_children_list_authenticated_related_parent(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -497,6 +508,7 @@ def test_api_documents_children_list_authenticated_related_parent(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -609,6 +621,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -631,6 +644,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -38,6 +38,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -62,6 +63,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -84,6 +86,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -135,6 +138,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -157,6 +161,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 5,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -179,6 +184,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -251,6 +257,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -273,6 +280,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -295,6 +303,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -352,6 +361,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -374,6 +384,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 5,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -396,6 +407,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -474,6 +486,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -496,6 +509,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -518,6 +532,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -576,6 +591,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -598,6 +614,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 5,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -620,6 +637,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -724,6 +742,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -746,6 +765,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -768,6 +788,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -293,3 +293,28 @@ def test_api_documents_duplicate_non_root_document(role):
assert duplicated_accesses.count() == 0
assert duplicated_document.is_sibling_of(child)
assert duplicated_document.is_child_of(document)
def test_api_documents_duplicate_reader_non_root_document():
"""
Reader users should be able to duplicate non-root documents but will be
created as a root document.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[(user, "reader")])
child = factories.DocumentFactory(parent=document)
assert child.get_role(user) == "reader"
response = client.post(
f"/api/v1.0/documents/{child.id!s}/duplicate/", format="json"
)
assert response.status_code == 201
duplicated_document = models.Document.objects.get(id=response.json()["id"])
assert duplicated_document.is_root()
assert duplicated_document.accesses.count() == 1
assert duplicated_document.accesses.get(user=user).role == "owner"
@@ -65,6 +65,7 @@ def test_api_document_favorite_list_authenticated_with_favorite():
"computed_link_role": document.computed_link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"content": document.content,
"depth": document.depth,
"excerpt": document.excerpt,
@@ -133,7 +133,10 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED,
link_role=models.LinkRoleChoices.READER,
)
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
@@ -143,7 +146,10 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
)
new_document_values = serializers.LinkDocumentSerializer(
instance=factories.DocumentFactory()
instance=factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.EDITOR,
)
).data
with mock_reset_connections(document.id):
@@ -158,3 +164,240 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
document_values = serializers.LinkDocumentSerializer(instance=document).data
for key, value in document_values.items():
assert value == new_document_values[key]
def test_api_documents_link_configuration_update_role_restricted_forbidden():
"""
Test that trying to set link_role on a document with restricted link_reach
returns a validation error.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.RESTRICTED,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
# Try to set a meaningful role on a restricted document
new_data = {
"link_reach": models.LinkReachChoices.RESTRICTED,
"link_role": models.LinkRoleChoices.EDITOR,
}
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_role" in response.json()
assert (
"Cannot set link_role when link_reach is 'restricted'"
in response.json()["link_role"][0]
)
def test_api_documents_link_configuration_update_link_reach_required():
"""
Test that link_reach is required when updating link configuration.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
# Try to update without providing link_reach
new_data = {"link_role": models.LinkRoleChoices.EDITOR}
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_reach" in response.json()
assert "This field is required" in response.json()["link_reach"][0]
def test_api_documents_link_configuration_update_restricted_without_role_success(
mock_reset_connections, # pylint: disable=redefined-outer-name
):
"""
Test that setting link_reach to restricted without specifying link_role succeeds.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
# Only specify link_reach, not link_role
new_data = {
"link_reach": models.LinkReachChoices.RESTRICTED,
}
with mock_reset_connections(document.id):
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 200
document.refresh_from_db()
assert document.link_reach == models.LinkReachChoices.RESTRICTED
@pytest.mark.parametrize(
"reach", [models.LinkReachChoices.PUBLIC, models.LinkReachChoices.AUTHENTICATED]
)
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
def test_api_documents_link_configuration_update_non_restricted_with_valid_role_success(
reach,
role,
mock_reset_connections, # pylint: disable=redefined-outer-name
):
"""
Test that setting non-restricted link_reach with valid link_role succeeds.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.RESTRICTED,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=document, user=user, role=models.RoleChoices.OWNER
)
new_data = {
"link_reach": reach,
"link_role": role,
}
with mock_reset_connections(document.id):
response = client.put(
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 200
document.refresh_from_db()
assert document.link_reach == reach
assert document.link_role == role
def test_api_documents_link_configuration_update_with_ancestor_constraints():
"""
Test that link configuration respects ancestor constraints using get_select_options.
This test may need adjustment based on the actual get_select_options implementation.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent_document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
child_document = factories.DocumentFactory(
parent=parent_document,
link_reach=models.LinkReachChoices.PUBLIC,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=child_document, user=user, role=models.RoleChoices.OWNER
)
# Try to set child to PUBLIC when parent is RESTRICTED
new_data = {
"link_reach": models.LinkReachChoices.RESTRICTED,
"link_role": models.LinkRoleChoices.READER,
}
response = client.put(
f"/api/v1.0/documents/{child_document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_reach" in response.json()
assert (
"Link reach 'restricted' is not allowed based on parent"
in response.json()["link_reach"][0]
)
def test_api_documents_link_configuration_update_invalid_role_for_reach_validation():
"""
Test the specific validation logic that checks if link_role is allowed for link_reach.
This tests the code section that validates allowed_roles from get_select_options.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent_document = factories.DocumentFactory(
link_reach=models.LinkReachChoices.AUTHENTICATED,
link_role=models.LinkRoleChoices.EDITOR,
)
child_document = factories.DocumentFactory(
parent=parent_document,
link_reach=models.LinkReachChoices.RESTRICTED,
link_role=models.LinkRoleChoices.READER,
)
factories.UserDocumentAccessFactory(
document=child_document, user=user, role=models.RoleChoices.OWNER
)
new_data = {
"link_reach": models.LinkReachChoices.AUTHENTICATED,
"link_role": models.LinkRoleChoices.READER, # This should be rejected
}
response = client.put(
f"/api/v1.0/documents/{child_document.id!s}/link-configuration/",
new_data,
format="json",
)
assert response.status_code == 400
assert "link_role" in response.json()
error_message = response.json()["link_role"][0]
assert (
"Link role 'reader' is not allowed for link reach 'authenticated'"
in error_message
)
assert "Allowed roles: editor" in error_message
@@ -69,6 +69,7 @@ def test_api_documents_list_format():
"computed_link_role": document.computed_link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": True,
@@ -70,6 +70,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -144,6 +145,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -252,6 +254,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"link_reach": reach,
@@ -333,6 +336,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"link_reach": document.link_reach,
@@ -446,6 +450,7 @@ def test_api_documents_retrieve_authenticated_related_direct():
"content": document.content,
"creator": str(document.creator.id),
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -528,6 +533,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"creator": str(document.creator.id),
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"depth": 3,
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"link_reach": "restricted",
@@ -683,6 +689,7 @@ def test_api_documents_retrieve_authenticated_related_team_members(
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -749,6 +756,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -815,6 +823,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -48,11 +48,11 @@ def test_api_documents_trashbin_format():
other_users = factories.UserFactory.create_batch(3)
document = factories.DocumentFactory(
deleted_at=timezone.now(),
users=factories.UserFactory.create_batch(2),
favorited_by=[user, *other_users],
link_traces=other_users,
)
document.soft_delete()
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
response = client.get("/api/v1.0/documents/trashbin/")
@@ -70,40 +70,40 @@ def test_api_documents_trashbin_format():
assert results[0] == {
"id": str(document.id),
"abilities": {
"accesses_manage": True,
"accesses_view": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"can_edit": True,
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": True,
"duplicate": True,
"favorite": True,
"invite_owner": True,
"link_configuration": True,
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
"children_list": False,
"collaboration_auth": False,
"descendants": False,
"cors_proxy": False,
"content": False,
"destroy": False,
"duplicate": False,
"favorite": False,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": True,
"media_auth": True,
"media_check": True,
"mask": False,
"media_auth": False,
"media_check": False,
"move": False, # Can't move a deleted document
"partial_update": True,
"partial_update": False,
"restore": True,
"retrieve": True,
"tree": True,
"update": True,
"versions_destroy": True,
"versions_list": True,
"versions_retrieve": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
},
"ancestors_link_reach": None,
"ancestors_link_role": None,
@@ -113,6 +113,7 @@ def test_api_documents_trashbin_format():
"creator": str(document.creator.id),
"depth": 1,
"excerpt": document.excerpt,
"deleted_at": document.ancestors_deleted_at.isoformat().replace("+00:00", "Z"),
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 0,
@@ -165,10 +166,10 @@ def test_api_documents_trashbin_authenticated_direct(django_assert_num_queries):
expected_ids = {str(document1.id), str(document2.id), str(document3.id)}
with django_assert_num_queries(10):
with django_assert_num_queries(11):
response = client.get("/api/v1.0/documents/trashbin/")
with django_assert_num_queries(4):
with django_assert_num_queries(5):
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
@@ -207,10 +208,10 @@ def test_api_documents_trashbin_authenticated_via_team(
expected_ids = {str(deleted_document_team1.id), str(deleted_document_team2.id)}
with django_assert_num_queries(7):
with django_assert_num_queries(8):
response = client.get("/api/v1.0/documents/trashbin/")
with django_assert_num_queries(3):
with django_assert_num_queries(4):
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
@@ -292,3 +293,26 @@ def test_api_documents_trashbin_distinct():
content = response.json()
assert len(content["results"]) == 1
assert content["results"][0]["id"] == str(document.id)
def test_api_documents_trashbin_empty_queryset_bug():
"""
Test that users with no owner role don't see documents.
"""
# Create a new user with no owner access to any document
new_user = factories.UserFactory()
client = APIClient()
client.force_login(new_user)
# Create some deleted documents owned by other users
other_user = factories.UserFactory()
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
content = response.json()
assert content["count"] == 0
assert len(content["results"]) == 0
@@ -50,6 +50,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -73,6 +74,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -96,6 +98,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling1.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling1.excerpt,
"id": str(sibling1.id),
"is_favorite": False,
@@ -119,6 +122,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling2.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling2.excerpt,
"id": str(sibling2.id),
"is_favorite": False,
@@ -138,6 +142,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -210,6 +215,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(child.creator.id),
"depth": 5,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -233,6 +239,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(document.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -260,6 +267,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(document_sibling.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
@@ -281,6 +289,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -306,6 +315,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(parent_sibling.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
@@ -327,6 +337,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_parent.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
@@ -406,6 +417,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -427,6 +439,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -450,6 +463,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
@@ -469,6 +483,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -546,6 +561,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(child.creator.id),
"depth": 5,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -569,6 +585,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(document.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -596,6 +613,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(document_sibling.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
@@ -617,6 +635,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -642,6 +661,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(parent_sibling.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
@@ -663,6 +683,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_parent.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
@@ -744,6 +765,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -765,6 +787,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -788,6 +811,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
@@ -807,6 +831,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -888,6 +913,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(child.creator.id),
"depth": 5,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -911,6 +937,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(document.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -938,6 +965,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(document_sibling.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
@@ -959,6 +987,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -984,6 +1013,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(parent_sibling.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
@@ -1005,6 +1035,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_parent.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
@@ -1094,6 +1125,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -1115,6 +1147,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -1138,6 +1171,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
@@ -1157,6 +1191,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -1170,3 +1205,56 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
"user_role": access.role,
}
def test_api_documents_tree_list_deleted_document():
"""
Tree of a deleted document should only be accessible to the owner.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent = factories.DocumentFactory(link_reach="public")
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
factories.DocumentFactory(link_reach="public", parent=document)
document.soft_delete()
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
assert response.status_code == 403
def test_api_documents_tree_list_deleted_document_owner(django_assert_num_queries):
"""
Tree of a deleted document should only be accessible to the owner.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent = factories.DocumentFactory(link_reach="public", users=[(user, "owner")])
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
child = factories.DocumentFactory(parent=document)
document.soft_delete()
document.refresh_from_db()
child.refresh_from_db()
with django_assert_num_queries(9):
client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
with django_assert_num_queries(5):
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
assert response.status_code == 200
content = response.json()
assert content["id"] == str(document.id)
assert content["deleted_at"] == document.deleted_at.isoformat().replace(
"+00:00", "Z"
)
assert len(content["children"]) == 1
assert content["children"][0]["id"] == str(child.id)
assert content["children"][0][
"deleted_at"
] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z")
+2 -1
View File
@@ -42,6 +42,7 @@ def test_api_config(is_authenticated):
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
assert response.json() == {
"AI_FEATURE_ENABLED": False,
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
"CRISP_WEBSITE_ID": "123",
@@ -60,7 +61,7 @@ def test_api_config(is_authenticated):
"MEDIA_BASE_URL": "http://testserver/",
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
"SENTRY_DSN": "https://sentry.test/123",
"AI_FEATURE_ENABLED": False,
"TRASHBIN_CUTOFF_DAYS": 30,
"theme_customization": {},
}
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
@@ -375,8 +375,42 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
document.soft_delete()
document.refresh_from_db()
expected_abilities["move"] = False
assert document.get_abilities(user) == expected_abilities
assert document.get_abilities(user) == {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
"children_list": False,
"collaboration_auth": False,
"descendants": False,
"cors_proxy": False,
"content": False,
"destroy": False,
"duplicate": False,
"favorite": False,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,
"media_auth": False,
"media_check": False,
"move": False,
"partial_update": False,
"restore": True,
"retrieve": True,
"tree": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
}
@override_settings(
+31 -1
View File
@@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError
import pytest
from core import factories
from core import factories, models
pytestmark = pytest.mark.django_db
@@ -66,3 +66,33 @@ def test_models_users_sub_validator(sub, is_valid):
match=("Enter a valid sub. This value should be ASCII only."),
):
user.full_clean()
def test_modes_users_convert_valid_invitations():
"""
The "convert_valid_invitations" method should convert valid invitations to document accesses.
"""
email = "test@example.com"
document = factories.DocumentFactory()
other_document = factories.DocumentFactory()
invitation_document = factories.InvitationFactory(email=email, document=document)
invitation_other_document = factories.InvitationFactory(
email="Test@example.coM", document=other_document
)
other_email_invitation = factories.InvitationFactory(
email="pre_test@example.com", document=document
)
assert document.accesses.count() == 0
assert other_document.accesses.count() == 0
user = factories.UserFactory(email=email)
assert document.accesses.filter(user=user).count() == 1
assert other_document.accesses.filter(user=user).count() == 1
assert not models.Invitation.objects.filter(id=invitation_document.id).exists()
assert not models.Invitation.objects.filter(
id=invitation_other_document.id
).exists()
assert models.Invitation.objects.filter(id=other_email_invitation.id).exists()
+2 -2
View File
@@ -142,7 +142,7 @@ class Base(Configuration):
)
# Document images
DOCUMENT_IMAGE_MAX_SIZE = values.Value(
DOCUMENT_IMAGE_MAX_SIZE = values.IntegerValue(
10 * (2**20), # 10MB
environ_name="DOCUMENT_IMAGE_MAX_SIZE",
environ_prefix=None,
@@ -490,7 +490,7 @@ class Base(Configuration):
environ_prefix=None,
)
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value(
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue(
60 * 60 * 24,
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
environ_prefix=None,
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -50,27 +50,36 @@ msgstr "Kuzhet"
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
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:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
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:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Korf"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Doare korf"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -225,8 +234,8 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "titl"
@@ -242,174 +251,174 @@ msgstr "Restr"
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1022 core/models.py:1022
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo ar postel"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Digeriñ"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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, 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/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -225,8 +234,8 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "Titel"
@@ -242,174 +251,174 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, 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:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
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:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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:1022 core/models.py:1022
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
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:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo-E-Mail"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Öffnen"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr ""
@@ -242,174 +251,174 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
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
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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 ""
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+66 -57
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
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:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Cuerpo"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Tipo de Cuerpo"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -163,7 +172,7 @@ msgstr "sub (UUID)"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
@@ -225,8 +234,8 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "título"
@@ -242,174 +251,174 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
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:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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:1022 core/models.py:1022
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
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:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
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:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
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:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo de correo electrónico"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Abrir"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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, su nueva herramienta esencial para organizar, compartir y colaborar en sus documentos como equipo."
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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 11:45\n"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -50,27 +50,36 @@ msgstr "Masqué"
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
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:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Corps"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Type de corps"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -225,8 +234,8 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "titre"
@@ -242,174 +251,174 @@ msgstr "Document"
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, 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:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
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:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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:1022 core/models.py:1022
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
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:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
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:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, 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:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, 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:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
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:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo de l'e-mail"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Ouvrir"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -225,8 +234,8 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "titolo"
@@ -242,174 +251,174 @@ msgstr "Documento"
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, 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:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo e-mail"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Apri"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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 ""
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+69 -60
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -44,33 +44,42 @@ msgstr "Ik ben Eigenaar"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
msgstr "Gemaskeerd"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriete"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document was gecreëerd voor u!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Text"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Text type"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -163,7 +172,7 @@ msgstr "id"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
msgstr "Vereist. Minder dan 255 ASCII tekens."
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
@@ -225,8 +234,8 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "titel"
@@ -242,174 +251,174 @@ msgstr "Document"
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, 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:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
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:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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:1022 core/models.py:1022
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
msgid "This team is already in this document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
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:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
msgstr "Document verzoekt om toegang"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
msgstr "Document verzoekt om toegangen"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
msgstr "{name} wenst toegang tot een document!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
msgstr "{name} wenst toegang tot het volgende document:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
msgstr "{name} vraagt toegang tot het document: {title}"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
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:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Template"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Templates"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "email adres"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo email"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Open"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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, jouw nieuwe essentiële tool voor het organiseren, delen en collaboreren van documenten als team. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "A propriedade de um novo documento foi concedida a você:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Tipo de corpo"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -225,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr ""
@@ -242,174 +251,174 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
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
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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 ""
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -50,27 +50,36 @@ msgstr "Скрытый"
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Текст сообщения"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Тип сообщения"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -225,8 +234,8 @@ msgstr "пользователь"
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "заголовок"
@@ -242,174 +251,174 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
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
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, 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
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип email"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Открыть"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
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:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
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:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Telo"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Vrsta telesa"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +234,8 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "naslov"
@@ -242,174 +251,174 @@ msgstr "Dokument"
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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:1022 core/models.py:1022
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1041 build/lib/core/models.py:1366
#: core/models.py:1041 core/models.py:1366
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
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:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "E-pošta z logotipom"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Odpri"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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 " Dokumenti, vaše novo bistveno orodje za organiziranje, skupno rabo in skupinsko sodelovanje pri dokumentih. "
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
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:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr ""
@@ -242,174 +251,174 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
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
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logotyp e-post"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Öppna"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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 ""
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -50,27 +50,36 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -225,8 +234,8 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr ""
@@ -242,174 +251,174 @@ msgstr ""
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:863 core/models.py:863
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
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
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr ""
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr ""
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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 ""
#: core/templates/mail/html/template.html:233
#: core/templates/mail/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+61 -52
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -50,27 +50,36 @@ msgstr "Приховано"
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "Вміст"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "Тип вмісту"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -225,8 +234,8 @@ msgstr "користувач"
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "заголовок"
@@ -242,174 +251,174 @@ msgstr "Документ"
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
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
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, 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
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип пошти"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Відкрити"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+69 -60
View File
@@ -2,8 +2,8 @@ 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"
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
"PO-Revision-Date: 2025-10-14 13:09\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -44,33 +44,42 @@ msgstr "创建者是我"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr ""
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
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
msgid "A new document was created on your behalf!"
msgstr "已为您创建了一份新文档!"
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
msgid "You have been granted ownership of a new document:"
msgstr "您已被授予新文档的所有权:"
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "必填字段。"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:960 core/api/viewsets.py:960
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -163,7 +172,7 @@ msgstr "sub"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
@@ -225,8 +234,8 @@ msgstr "用户"
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:359 build/lib/core/models.py:1280
#: core/models.py:359 core/models.py:1280
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
#: core/models.py:359 core/models.py:1282
msgid "title"
msgstr "标题"
@@ -242,174 +251,174 @@ msgstr "文档"
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:422 build/lib/core/models.py:818 core/models.py:422
#: core/models.py:818
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
#: core/models.py:820
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:853 core/models.py:853
#: build/lib/core/models.py:855 core/models.py:855
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:859 core/models.py:859
#, 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
#: build/lib/core/models.py:865 core/models.py:865
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:963 core/models.py:963
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:964 core/models.py:964
#: build/lib/core/models.py:966 core/models.py:966
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:970 core/models.py:970
#: build/lib/core/models.py:972 core/models.py:972
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:993 core/models.py:993
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:994 core/models.py:994
#: build/lib/core/models.py:996 core/models.py:996
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1000 core/models.py:1000
#: build/lib/core/models.py:1002 core/models.py:1002
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
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1023 core/models.py:1023
#: build/lib/core/models.py:1025 core/models.py:1025
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1029 core/models.py:1029
#: build/lib/core/models.py:1031 core/models.py:1031
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1035 core/models.py:1035
#: build/lib/core/models.py:1037 core/models.py:1037
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
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
#: core/models.py:1043 core/models.py:1368
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1187 core/models.py:1187
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for access"
msgstr ""
msgstr "文档需要访问权限"
#: build/lib/core/models.py:1188 core/models.py:1188
#: build/lib/core/models.py:1190 core/models.py:1190
msgid "Document ask for accesses"
msgstr ""
msgstr "文档需要访问权限"
#: build/lib/core/models.py:1194 core/models.py:1194
#: build/lib/core/models.py:1196 core/models.py:1196
msgid "This user has already asked for access to this document."
msgstr ""
msgstr "用户已申请该文档的访问权限。"
#: build/lib/core/models.py:1259 core/models.py:1259
#: build/lib/core/models.py:1261 core/models.py:1261
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
msgstr "{name} 申请访问文档!"
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1265 core/models.py:1265
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
msgstr "{name} 申请访问以下文档:"
#: build/lib/core/models.py:1269 core/models.py:1269
#: build/lib/core/models.py:1271 core/models.py:1271
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
msgstr "{name}申请文档:{title}的访问权限"
#: build/lib/core/models.py:1281 core/models.py:1281
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1282 core/models.py:1282
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1283 core/models.py:1283
#: build/lib/core/models.py:1285 core/models.py:1285
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1287 core/models.py:1287
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1289 core/models.py:1289
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1293 core/models.py:1293
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1294 core/models.py:1294
#: build/lib/core/models.py:1296 core/models.py:1296
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1348 core/models.py:1348
#: build/lib/core/models.py:1350 core/models.py:1350
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1354 core/models.py:1354
#: build/lib/core/models.py:1356 core/models.py:1356
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1360 core/models.py:1360
#: build/lib/core/models.py:1362 core/models.py:1362
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1437 core/models.py:1437
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1456 core/models.py:1456
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1457 core/models.py:1457
#: build/lib/core/models.py:1459 core/models.py:1459
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1477 core/models.py:1477
#: build/lib/core/models.py:1479 core/models.py:1479
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
#: core/templates/mail/html/template.html:162
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "徽标邮件"
#: core/templates/mail/html/template.html:209
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "打开"
#: core/templates/mail/html/template.html:226
#: core/templates/mail/html/template.html:217
#: 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/html/template.html:224
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+2 -2
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.6.0"
version = "3.8.21"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -39,7 +39,7 @@ dependencies = [
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.2.6",
"django==5.2.7",
"django-treebeard==4.7.1",
"djangorestframework==3.16.0",
"drf_spectacular==0.28.0",
+7 -1
View File
@@ -50,7 +50,13 @@ ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
RUN yarn build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
FROM nginxinc/nginx-unprivileged:alpine3.22 AS frontend-production
# Upgrade system packages to install security updates
USER root
RUN apk update && \
apk upgrade && \
rm -rf /var/cache/apk/*
# Un-privileged user running the application
ARG DOCKER_USER
@@ -89,8 +89,8 @@ test.describe('Doc Create: Not logged', () => {
const data = {
title,
content: markdown,
sub: `user@${browserName}.test`,
email: `user@${browserName}.test`,
sub: `user.test@${browserName}.test`,
email: `user.test@${browserName}.test`,
};
const newDoc = await request.post(
@@ -1,7 +1,7 @@
/* eslint-disable playwright/no-conditional-expect */
import path from 'path';
import { chromium, expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import {
@@ -11,7 +11,9 @@ import {
overrideConfig,
verifyDocName,
} from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
import { createRootSubPage, navigateToPageFromTree } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -86,8 +88,7 @@ test.describe('Doc Editor', () => {
// Is connected
let framesentPromise = webSocket.waitForEvent('framesent');
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await writeInEditor({ page, text: 'Hello World' });
let framesent = await framesentPromise;
expect(framesent.payload).not.toBeNull();
@@ -100,7 +101,7 @@ test.describe('Doc Editor', () => {
const wsClosePromise = webSocket.waitForEvent('close');
await selectVisibility.click();
await page.getByLabel('Connected').click();
await page.getByRole('menuitem', { name: 'Connected' }).click();
// Assert that the doc reconnects to the ws
const wsClose = await wsClosePromise;
@@ -238,17 +239,7 @@ test.describe('Doc Editor', () => {
test('it cannot edit if viewer', async ({ page }) => {
await mockedDocument(page, {
abilities: {
destroy: false, // Means not owner
link_configuration: false,
versions_destroy: false,
versions_list: true,
versions_retrieve: true,
accesses_manage: false, // Means not admin
update: false,
partial_update: false, // Means not editor
retrieve: true,
},
user_role: 'reader',
});
await goToGridDoc(page);
@@ -257,6 +248,9 @@ test.describe('Doc Editor', () => {
await expect(card).toBeVisible();
await expect(card.getByText('Reader')).toBeVisible();
const editor = page.locator('.ProseMirror');
await expect(editor).toHaveAttribute('contenteditable', 'false');
});
test('it adds an image to the doc editor', async ({ page, browserName }) => {
@@ -512,10 +506,7 @@ test.describe('Doc Editor', () => {
await verifyDocName(page, randomDoc);
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.locator('.bn-block-outer').last().fill('/');
const editor = await openSuggestionMenu({ page });
await page.getByText('Embedded file').click();
await page.getByText('Upload file').click();
@@ -570,20 +561,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-visibility').click();
await page
.getByRole('menuitem', {
name: 'Public',
})
.click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await updateShareLink(page, 'Public', 'Editing');
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
@@ -607,17 +585,12 @@ test.describe('Doc Editor', () => {
* We open another browser that will connect to the collaborative server
* and will block the current browser to edit the doc.
*/
const otherBrowser = await chromium.launch({ headless: true });
const otherContext = await otherBrowser.newContext({
locale: 'en-US',
timezoneId: 'Europe/Paris',
permissions: [],
storageState: {
cookies: [],
origins: [],
},
const { otherPage } = await connectOtherUserToDoc({
browserName,
docUrl: urlChildDoc,
docTitle: childTitle,
withoutSignIn: true,
});
const otherPage = await otherContext.newPage();
const webSocketPromise = otherPage.waitForEvent(
'websocket',
@@ -658,6 +631,11 @@ test.describe('Doc Editor', () => {
await expect(editor).toHaveAttribute('contenteditable', 'false');
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toBeHidden();
await expect(page.getByRole('heading', { name: childTitle })).toBeVisible();
await page.goto(urlParentDoc);
await verifyDocName(page, parentTitle);
@@ -674,6 +652,11 @@ test.describe('Doc Editor', () => {
await expect(editor).toHaveAttribute('contenteditable', 'true');
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText(childTitle);
await expect(page.getByRole('heading', { name: childTitle })).toBeHidden();
await expect(
card.getByText('Others are editing. Your network prevent changes.'),
).toBeHidden();
@@ -682,9 +665,7 @@ test.describe('Doc Editor', () => {
test('it checks if callout custom block', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
const editor = page.locator('.ProseMirror');
await editor.click();
await page.locator('.bn-block-outer').last().fill('/');
await openSuggestionMenu({ page });
await page.getByText('Add a callout block').click();
const calloutBlock = page
@@ -703,8 +684,20 @@ test.describe('Doc Editor', () => {
const emojiButton = calloutBlock.getByRole('button');
await expect(emojiButton).toHaveText('💡');
await emojiButton.click();
await page.locator('button[aria-label="⚠️"]').click();
await expect(emojiButton).toHaveText('⚠️');
// Group smiley
await expect(page.getByRole('button', { name: '🤠' })).toBeVisible();
// Group animals
await page.getByText('Animals & Nature').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🦆' })).toBeVisible();
// Group travel
await page.getByText('Travel & Places').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🚝' })).toBeVisible();
// Group objects
await page.getByText('Objects').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🪇' })).toBeVisible();
// Group symbol
await page.getByText('Symbols').scrollIntoViewIfNeeded();
await expect(page.getByRole('button', { name: '🛃' })).toBeVisible();
await page.locator('.bn-side-menu > button').last().click();
await page.locator('.mantine-Menu-dropdown > button').last().click();
@@ -757,15 +750,21 @@ test.describe('Doc Editor', () => {
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('ArrowDown');
await page.keyboard.press('Enter');
const interlink = page.getByRole('link', {
name: 'child-2',
// Wait for the search container to disappear, indicating selection was made
await expect(searchContainer).toBeHidden();
// Wait for the interlink to be created and rendered
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('button', {
name: docChild2,
});
await expect(interlink).toBeVisible();
await expect(interlink).toBeVisible({ timeout: 10000 });
await interlink.click();
await verifyDocName(page, docChild2);
@@ -786,4 +785,93 @@ test.describe('Doc Editor', () => {
),
).toBeVisible();
});
test('it checks multiple big doc scroll to the top', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(page, 'doc-scroll', browserName, 1);
for (let i = 0; i < 15; i++) {
await page.keyboard.press('Enter');
await writeInEditor({ page, text: 'Hello Parent ' + i });
}
const editor = await getEditor({ page });
await expect(
editor.getByText('Hello Parent 1', { exact: true }),
).not.toBeInViewport();
await expect(editor.getByText('Hello Parent 14')).toBeInViewport();
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-scroll-child',
);
for (let i = 0; i < 15; i++) {
await page.keyboard.press('Enter');
await writeInEditor({ page, text: 'Hello Child ' + i });
}
await expect(
editor.getByText('Hello Child 1', { exact: true }),
).not.toBeInViewport();
await expect(editor.getByText('Hello Child 14')).toBeInViewport();
await navigateToPageFromTree({ page, title: randomDoc });
await expect(
editor.getByText('Hello Parent 1', { exact: true }),
).toBeInViewport();
await expect(editor.getByText('Hello Parent 14')).not.toBeInViewport();
await navigateToPageFromTree({ page, title: docChild });
await expect(
editor.getByText('Hello Child 1', { exact: true }),
).toBeInViewport();
await expect(editor.getByText('Hello Child 14')).not.toBeInViewport();
});
test('it embeds PDF', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
await openSuggestionMenu({ page });
await page.getByText('Embed a PDF file').click();
const pdfBlock = page.locator('div[data-content-type="pdf"]').first();
await expect(pdfBlock).toBeVisible();
await page.getByText('Add PDF').click();
const fileChooserPromise = page.waitForEvent('filechooser');
const downloadPromise = page.waitForEvent('download');
await page.getByText('Upload file').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf'));
// Wait for the media-check to be processed
await page.waitForTimeout(1000);
const pdfEmbed = page
.locator('.--docs--editor-container embed.bn-visual-media')
.first();
// Check src of pdf
expect(await pdfEmbed.getAttribute('src')).toMatch(
/http:\/\/localhost:8083\/media\/.*\/attachments\/.*.pdf/,
);
await expect(pdfEmbed).toHaveAttribute('type', 'application/pdf');
await expect(pdfEmbed).toHaveAttribute('role', 'presentation');
// Check download with original filename
await page.locator('.bn-block-content[data-content-type="pdf"]').click();
await page.locator('[data-test="downloadfile"]').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('test-pdf.pdf');
});
});
@@ -11,6 +11,7 @@ import {
verifyDocName,
waitForLanguageSwitch,
} from './utils-common';
import { openSuggestionMenu, writeInEditor } from './utils-editor';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
@@ -93,6 +94,7 @@ test.describe('Doc Export', () => {
expect(pdfData.numpages).toBe(2);
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
expect(pdfData.info.Title).toBe(randomDoc);
});
test('it exports the doc to docx', async ({ page, browserName }) => {
@@ -152,11 +154,13 @@ test.describe('Doc Export', () => {
await verifyDocName(page, randomDoc);
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await writeInEditor({
page,
text: 'Hello World 😃🎉🚀🙋‍♀️🧑🏿‍❤️‍💋‍🧑🏾',
});
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
await openSuggestionMenu({ page });
await page.getByText('Resizable image with caption').click();
const fileChooserPromise = page.waitForEvent('filechooser');
@@ -393,7 +397,7 @@ test.describe('Doc Export', () => {
})
.click();
const input = page.locator('.--docs--doc-title-input[role="textbox"]');
const input = page.getByRole('textbox', { name: 'Titre du document' });
await expect(input).toBeVisible();
await expect(input).toHaveText('', { timeout: 10000 });
await input.click();
@@ -410,6 +414,10 @@ test.describe('Doc Export', () => {
})
.click();
await expect(
page.getByTestId('doc-open-modal-download-button'),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
});
@@ -464,7 +472,7 @@ test.describe('Doc Export', () => {
// Search the interlinking link in the editor (not in the document tree)
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('link', {
const interlink = editor.getByRole('button', {
name: randomDoc,
});
@@ -106,7 +106,7 @@ test.describe('Doc grid dnd', () => {
await expect(dragOverlay).toBeVisible();
await expect(dragOverlay).toHaveText(
'You must be at least the editor of the target document',
'You must be at least the administrator of the target document',
);
await page.mouse.up();
@@ -28,7 +28,7 @@ test.describe('Documents Grid mobile', () => {
id: '8c1e047a-24e7-4a80-942b-8e9c7ab43e1f',
user: {
id: '7380f42f-02eb-4ad5-b8f0-037a0e66066d',
email: 'test@test.test',
email: 'test.test@test.test',
full_name: 'John Doe',
short_name: 'John',
},
@@ -117,7 +117,7 @@ test.describe('Document grid item options', () => {
await page.getByText('push_pin').click();
// Check is pinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
await expect(row.getByTestId('doc-pinned-icon')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
@@ -126,7 +126,7 @@ test.describe('Document grid item options', () => {
await page.getByText('Unpin').click();
// Check is unpinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(row.getByTestId('doc-pinned-icon')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -139,7 +139,7 @@ test.describe('Document grid item options', () => {
const row = await getGridRow(page, docTitle);
await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Remove' }).click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await expect(
page.getByRole('heading', { name: 'Delete a doc' }),
@@ -75,28 +75,28 @@ test.describe('Doc Header', () => {
// 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 expect(docTree.getByTestId('doc-emoji-icon')).toBeVisible();
await expect(docTree.getByTestId('doc-simple-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();
await expect(gridRow.getByTestId('doc-emoji-icon')).toBeVisible();
await expect(gridRow.getByTestId('doc-simple-icon')).toBeHidden();
});
test('it deletes the doc', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
await page.getByLabel('Open the document options').click();
await page.getByLabel('Delete document').click();
await page.getByRole('menuitem', { name: 'Delete document' }).click();
await expect(
page.getByRole('heading', { name: 'Delete a doc' }),
).toBeVisible();
await expect(page.getByText(`This document and any sub-`)).toBeVisible();
await expect(page.getByText(`This document will be`)).toBeVisible();
await page
.getByRole('button', {
@@ -142,13 +142,19 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText('Mocked document');
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();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
@@ -164,7 +170,7 @@ test.describe('Doc Header', () => {
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test@invitation.test').first(),
invitationCard.getByText('test.test@invitation.test').first(),
).toBeVisible();
const invitationRole = invitationCard.getByLabel('doc-role-dropdown');
await expect(invitationRole).toBeVisible();
@@ -178,7 +184,7 @@ test.describe('Doc Header', () => {
const roles = memberCard.getByLabel('doc-role-dropdown');
await expect(memberCard).toBeVisible();
await expect(
memberCard.getByText('test@accesses.test').first(),
memberCard.getByText('test.test@accesses.test').first(),
).toBeVisible();
await expect(roles).toBeVisible();
@@ -216,12 +222,18 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(
page.getByRole('textbox', { name: 'Document title' }),
).toContainText('Mocked document');
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();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
@@ -239,7 +251,7 @@ test.describe('Doc Header', () => {
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test@invitation.test').first(),
invitationCard.getByText('test.test@invitation.test').first(),
).toBeVisible();
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
await expect(
@@ -247,7 +259,7 @@ test.describe('Doc Header', () => {
).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
await expect(memberCard.getByText('test.test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
await expect(
memberCard.getByRole('button', { name: 'more_horiz' }),
@@ -282,12 +294,18 @@ test.describe('Doc Header', () => {
await goToGridDoc(page);
await expect(
page.getByRole('heading', { name: 'Mocked document' }),
).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();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
@@ -302,7 +320,7 @@ test.describe('Doc Header', () => {
const invitationCard = shareModal.getByLabel('List invitation card');
await expect(invitationCard).toBeVisible();
await expect(
invitationCard.getByText('test@invitation.test').first(),
invitationCard.getByText('test.test@invitation.test').first(),
).toBeVisible();
await expect(invitationCard.getByLabel('Document role text')).toBeVisible();
await expect(
@@ -310,7 +328,7 @@ test.describe('Doc Header', () => {
).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
await expect(memberCard.getByText('test.test@accesses.test')).toBeVisible();
await expect(memberCard.getByLabel('Document role text')).toBeVisible();
await expect(
memberCard.getByRole('button', { name: 'more_horiz' }),
@@ -343,7 +361,7 @@ test.describe('Doc Header', () => {
// Copy content to clipboard
await page.getByLabel('Open the document options').click();
await page.getByLabel('Copy as Markdown').click();
await page.getByRole('menuitem', { name: 'Copy as Markdown' }).click();
await expect(page.getByText('Copied to clipboard')).toBeVisible();
// Test that clipboard is in Markdown format
@@ -377,7 +395,7 @@ test.describe('Doc Header', () => {
// Copy content to clipboard
await page.getByLabel('Open the document options').click();
await page.getByLabel('Copy as HTML').click();
await page.getByRole('menuitem', { name: 'Copy as HTML' }).click();
await expect(page.getByText('Copied to clipboard')).toBeVisible();
// Test that clipboard is in HTML format
@@ -434,11 +452,15 @@ test.describe('Doc Header', () => {
test('it pins a document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `Pin doc`, browserName);
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
// Pin
await page.getByText('push_pin').click();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await expect(page.getByText('Unpin')).toBeVisible();
await page.goto('/');
@@ -446,22 +468,26 @@ test.describe('Doc Header', () => {
const row = await getGridRow(page, docTitle);
// Check is pinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
await expect(row.getByTestId('doc-pinned-icon')).toBeVisible();
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
await row.getByText(docTitle).click();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
// Unpin
await page.getByText('Unpin').click();
await page.getByLabel('Open the document options').click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await expect(page.getByText('push_pin')).toBeVisible();
await page.goto('/');
// Check is unpinned
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
await expect(row.getByTestId('doc-pinned-icon')).toBeHidden();
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
});
@@ -560,7 +586,7 @@ test.describe('Documents Header mobile', () => {
await expect(
page.getByRole('menuitem', { name: 'Copy link' }),
).toBeVisible();
await page.getByLabel('Share').click();
await page.getByRole('menuitem', { name: 'Share' }).click();
await expect(page.getByRole('button', { name: 'Copy link' })).toBeVisible();
});
@@ -583,7 +609,7 @@ test.describe('Documents Header mobile', () => {
await goToGridDoc(page);
await page.getByLabel('Open the document options').click();
await page.getByLabel('Share').click();
await page.getByRole('menuitem', { name: 'Share' }).click();
const shareModal = page.getByRole('dialog', {
name: 'Share modal content',
@@ -18,7 +18,7 @@ test.describe('Inherited share accesses', () => {
).toBeVisible();
const user = page.getByTestId(
`doc-share-member-row-user@${browserName}.test`,
`doc-share-member-row-user.test@${browserName}.test`,
);
await expect(user).toBeVisible();
await expect(user.getByText('E2E Chromium')).toBeVisible();
@@ -7,6 +7,7 @@ import {
randomName,
verifyDocName,
} from './utils-common';
import { connectOtherUserToDoc, updateRoleUser } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Document create member', () => {
@@ -25,9 +26,8 @@ test.describe('Document create member', () => {
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
const inputSearch = page.getByTestId('quick-search-input');
await expect(inputSearch).toBeVisible();
// Select user 1 and verify tag
@@ -74,13 +74,15 @@ test.describe('Document create member', () => {
// Check roles are displayed
await list.getByLabel('doc-role-dropdown').click();
await expect(page.getByLabel('Reader')).toBeVisible();
await expect(page.getByLabel('Editor')).toBeVisible();
await expect(page.getByLabel('Owner')).toBeVisible();
await expect(page.getByLabel('Administrator')).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Administrator' }),
).toBeVisible();
// Validate
await page.getByLabel('Administrator').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await page.getByRole('button', { name: 'Invite' }).click();
// Check invitation added
@@ -117,9 +119,7 @@ test.describe('Document create member', () => {
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
const inputSearch = page.getByTestId('quick-search-input');
const [email] = randomName('test@test.fr', browserName, 1);
await inputSearch.fill(email);
@@ -128,7 +128,7 @@ test.describe('Document create member', () => {
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByLabel('doc-role-dropdown').click();
await page.getByLabel('Owner').click();
await page.getByRole('menuitem', { name: 'Owner' }).click();
const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
@@ -146,7 +146,7 @@ test.describe('Document create member', () => {
// Choose a role
await container.getByLabel('doc-role-dropdown').click();
await page.getByLabel('Owner').click();
await page.getByRole('menuitem', { name: 'Owner' }).click();
const responsePromiseCreateInvitationFail = page.waitForResponse(
(response) =>
@@ -167,18 +167,23 @@ test.describe('Document create member', () => {
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
const inputSearch = page.getByTestId('quick-search-input');
const email = randomName('test@test.fr', browserName, 1)[0];
let email = 'user.test21@example.COM';
await inputSearch.fill(email);
// Check email is found in search (case insensitive)
await expect(page.getByRole('option').getByText(email)).toHaveCount(1);
email = email + 'M';
await inputSearch.fill(email);
await page.getByTestId(`search-user-row-${email}`).click();
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByLabel('doc-role-dropdown').click();
await page.getByLabel('Administrator').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
@@ -193,26 +198,32 @@ test.describe('Document create member', () => {
const listInvitation = page.getByTestId('doc-share-quick-search');
const userInvitation = listInvitation.getByTestId(
`doc-share-invitation-row-${email}`,
`doc-share-invitation-row-${email.toLowerCase()}`,
);
await expect(userInvitation).toBeVisible();
const responsePromisePatchInvitation = page.waitForResponse(
(response) =>
response.url().includes('/invitations/') &&
response.status() === 200 &&
response.request().method() === 'PATCH',
);
await userInvitation.getByLabel('doc-role-dropdown').click();
await page.getByLabel('Reader').click();
await page.getByRole('menuitem', { name: 'Reader' }).click();
const responsePatchInvitation = await responsePromisePatchInvitation;
expect(responsePatchInvitation.ok()).toBeTruthy();
const moreActions = userInvitation.getByRole('button', {
name: 'more_horiz',
name: 'Open invitation actions menu',
});
await moreActions.click();
await page.getByLabel('Delete').click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await expect(userInvitation).toBeHidden();
});
});
test.describe('Document create member: Multiple login', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('It creates a member from a request coming from a 403 page', async ({
page,
@@ -220,9 +231,6 @@ test.describe('Document create member: Multiple login', () => {
}) => {
test.slow();
await page.goto('/');
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
page,
'Member access request',
@@ -232,67 +240,67 @@ test.describe('Document create member: Multiple login', () => {
await verifyDocName(page, docTitle);
const urlDoc = page.url();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
.locator('.ProseMirror')
.locator('.bn-block-outer')
.last()
.fill('Hello World');
const otherBrowser = BROWSERS.find((b) => b !== browserName);
const docUrl = page.url();
await keyCloakSignIn(page, otherBrowser!);
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
// Other user will request access
const { otherPage, otherBrowserName, cleanup } =
await connectOtherUserToDoc({ browserName, docUrl });
await expect(
page.getByText('Insufficient access rights to view the document.'),
otherPage.getByText('Insufficient access rights to view the document.'),
).toBeVisible({
timeout: 10000,
});
await page.getByRole('button', { name: 'Request access' }).click();
await otherPage.getByRole('button', { name: 'Request access' }).click();
await expect(
page.getByText('Your access request for this document is pending.'),
otherPage.getByText('Your access request for this document is pending.'),
).toBeVisible();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await page.goto('/');
await keyCloakSignIn(page, browserName);
await expect(page.getByTestId('header-logo-link')).toBeVisible({
timeout: 10000,
});
await page.goto(urlDoc);
// First user approves the request
await page.getByRole('button', { name: 'Share' }).click();
await expect(page.getByText('Access Requests')).toBeVisible();
await expect(page.getByText(`E2E ${otherBrowser}`)).toBeVisible();
await expect(page.getByText(`E2E ${otherBrowserName}`)).toBeVisible();
const emailRequest = `user@${otherBrowser}.test`;
const emailRequest = `user.test@${otherBrowserName}.test`;
await expect(page.getByText(emailRequest)).toBeVisible();
const container = page.getByTestId(
`doc-share-access-request-row-${emailRequest}`,
);
await container.getByLabel('doc-role-dropdown').click();
await page.getByLabel('Administrator').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await container.getByRole('button', { name: 'Approve' }).click();
await expect(page.getByText('Access Requests')).toBeHidden();
await expect(page.getByText('Share with 2 users')).toBeVisible();
await expect(page.getByText(`E2E ${otherBrowser}`)).toBeVisible();
await expect(page.getByText(`E2E ${otherBrowserName}`)).toBeVisible();
// Other user verifies he has access
await otherPage.reload();
await verifyDocName(otherPage, docTitle);
await expect(otherPage.getByText('Hello World')).toBeVisible();
// Revoke access
await updateRoleUser(page, 'Remove access', emailRequest);
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),
).toBeVisible();
// Cleanup: other user logout
await cleanup();
});
});
test.describe('Document create member: Multiple login', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('It cannot request member access on child doc on a 403 page', async ({
page,
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
import { createDoc, verifyDocName } from './utils-common';
import { addNewMember } from './utils-share';
test.beforeEach(async ({ page }) => {
@@ -8,8 +8,14 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Document list members', () => {
test('it checks a big list of members', async ({ page }) => {
const docTitle = await goToGridDoc(page);
test('it checks a big list of members', async ({ page, browserName }) => {
const [docTitle] = await createDoc(
page,
'members-big-members-list',
browserName,
1,
);
await verifyDocName(page, docTitle);
// Get the current URL and extract the last part
@@ -73,7 +79,7 @@ test.describe('Document list members', () => {
await expect(loadMore).toBeHidden();
});
test('it checks a big list of invitations', async ({ page }) => {
test('it checks a big list of invitations', async ({ page, browserName }) => {
await page.route(
/.*\/documents\/.*\/invitations\/\?page=.*/,
async (route) => {
@@ -108,7 +114,12 @@ test.describe('Document list members', () => {
},
);
const docTitle = await goToGridDoc(page);
const [docTitle] = await createDoc(
page,
'members-big-invitation-list',
browserName,
1,
);
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
@@ -139,7 +150,7 @@ test.describe('Document list members', () => {
const list = page.getByTestId('doc-share-quick-search');
await expect(list).toBeVisible();
const currentUser = list.getByTestId(
`doc-share-member-row-user@${browserName}.test`,
`doc-share-member-row-user.test@${browserName}.test`,
);
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await expect(currentUser).toBeVisible();
@@ -171,12 +182,12 @@ test.describe('Document list members', () => {
});
await currentUserRole.click();
await page.getByLabel('Administrator').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await list.click();
await expect(currentUserRole).toBeVisible();
await currentUserRole.click();
await page.getByLabel('Reader').click();
await page.getByRole('menuitem', { name: 'Reader' }).click();
await list.click();
await expect(currentUserRole).toBeHidden();
});
@@ -190,7 +201,7 @@ test.describe('Document list members', () => {
const list = page.getByTestId('doc-share-quick-search');
const emailMyself = `user@${browserName}.test`;
const emailMyself = `user.test@${browserName}.test`;
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
const mySelfRole = mySelf.getByRole('button', {
name: 'doc-role-dropdown',
@@ -60,39 +60,40 @@ test.describe('Doc Routing', () => {
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
// Wait for the doc link (via its dynamic title) to be visible
const docLink = page.getByRole('link', { name: docTitle });
await expect(docLink).toBeVisible();
const responsePromise = page.route(
/.*\/documents\/.*\/$|users\/me\/$/,
async (route) => {
const request = route.request();
// Intercept GET/PATCH requests to return 401
await page.route(/.*\/documents\/.*\/$|users\/me\/$/, async (route) => {
const request = route.request();
if (
request.method().includes('PATCH') ||
request.method().includes('GET')
) {
await route.fulfill({
status: 401,
json: { detail: 'Log in to access the document' },
});
} else {
await route.continue();
}
});
// Explicitly wait for a 401 response after clicking
const wait401 = page.waitForResponse(
(resp) =>
resp.status() === 401 &&
/\/(documents\/[^/]+\/|users\/me\/)$/.test(resp.url()),
if (
request.method().includes('PATCH') ||
request.method().includes('GET')
) {
await route.fulfill({
status: 401,
json: {
detail: 'Log in to access the document',
},
});
} else {
await route.continue();
}
},
);
await docLink.click();
await wait401;
await page.getByRole('link', { name: '401-doc-parent' }).click();
await responsePromise;
await expect(page.getByText('Log in to access the document.')).toBeVisible({
timeout: 10000,
});
await expect(page.locator('meta[name="robots"]')).toHaveAttribute(
'content',
'noindex',
);
await expect(page).toHaveTitle(/401 Unauthorized - Docs/);
});
});
@@ -8,8 +8,6 @@ test.beforeEach(async ({ page }) => {
test.describe('Doc Table Content', () => {
test('it checks the doc table content', async ({ page, browserName }) => {
test.setTimeout(60000);
const [randomDoc] = await createDoc(
page,
'doc-table-content',
@@ -0,0 +1,151 @@
import { expect, test } from '@playwright/test';
import {
clickInEditorMenu,
clickInGridMenu,
createDoc,
getGridRow,
verifyDocName,
} from './utils-common';
import { addNewMember } from './utils-share';
import {
addChild,
createRootSubPage,
navigateToPageFromTree,
} from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Trashbin', () => {
test('it controls UI and interaction from the grid page', async ({
page,
browserName,
}) => {
const [title1] = await createDoc(page, 'my-trash-doc-1', browserName, 1);
const [title2] = await createDoc(page, 'my-trash-doc-2', browserName, 1);
await verifyDocName(page, title2);
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Editor');
await page.getByRole('button', { name: 'close' }).click();
await page.getByRole('button', { name: 'Back to homepage' }).click();
const row1 = await getGridRow(page, title1);
await clickInGridMenu(page, row1, 'Delete');
await page.getByRole('button', { name: 'Delete document' }).click();
await expect(row1.getByText(title1)).toBeHidden();
const row2 = await getGridRow(page, title2);
await clickInGridMenu(page, row2, 'Delete');
await page.getByRole('button', { name: 'Delete document' }).click();
await expect(row2.getByText(title2)).toBeHidden();
await page.getByRole('link', { name: 'Trashbin' }).click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid.getByText('Days remaining')).toBeVisible();
await expect(row1.getByText(title1)).toBeVisible();
await expect(row1.getByText('30 days')).toBeVisible();
await expect(row2.getByText(title2)).toBeVisible();
await expect(
row2.getByRole('button', {
name: 'Open the sharing settings for the document',
}),
).toBeVisible();
await expect(
row2.getByRole('button', {
name: 'Open the sharing settings for the document',
}),
).toBeDisabled();
await clickInGridMenu(page, row2, 'Restore');
await expect(row2.getByText(title2)).toBeHidden();
await page.getByRole('link', { name: 'All docs' }).click();
const row2Restored = await getGridRow(page, title2);
await expect(row2Restored.getByText(title2)).toBeVisible();
await row2Restored.getByRole('link', { name: /Open document/ }).click();
await verifyDocName(page, title2);
await page.getByRole('button', { name: 'Back to homepage' }).click();
await expect(row2.getByText(title2)).toBeVisible();
await expect(
row2.getByRole('button', {
name: 'Open the sharing settings for the document',
}),
).toBeEnabled();
await page.getByRole('link', { name: 'Trashbin' }).click();
await expect(row2.getByText(title2)).toBeHidden();
});
test('it controls UI and interaction from the doc page', async ({
page,
browserName,
}) => {
const [topParent] = await createDoc(
page,
'my-trash-editor-doc',
browserName,
1,
);
await verifyDocName(page, topParent);
const { name: subDocName } = await createRootSubPage(
page,
browserName,
'my-trash-editor-subdoc',
);
const subsubDocName = await addChild({
page,
browserName,
docParent: subDocName,
docName: 'my-trash-editor-subsubdoc',
});
await verifyDocName(page, subsubDocName);
await navigateToPageFromTree({ page, title: subDocName });
await verifyDocName(page, subDocName);
await clickInEditorMenu(page, 'Delete sub-document');
await page.getByRole('button', { name: 'Delete document' }).click();
await verifyDocName(page, topParent);
await page.getByRole('button', { name: 'Back to homepage' }).click();
await page.getByRole('link', { name: 'Trashbin' }).click();
const row = await getGridRow(page, subDocName);
await row.getByText(subDocName).click();
await verifyDocName(page, subDocName);
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
await expect(page.locator('.bn-editor')).toHaveAttribute(
'contenteditable',
'false',
);
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(topParent)).toBeHidden();
await expect(
docTree.getByText(subDocName, {
exact: true,
}),
).toBeVisible();
await expect(docTree.getByText(subsubDocName)).toBeVisible();
await expect(
docTree
.locator(".--docs-sub-page-item[aria-disabled='true']")
.getByText(subsubDocName),
).toBeVisible();
await page.getByRole('button', { name: 'Restore' }).click();
await expect(page.getByLabel('Alert deleted document')).toBeHidden();
await expect(page.locator('.bn-editor')).toHaveAttribute(
'contenteditable',
'true',
);
await expect(page.getByRole('button', { name: 'Share' })).toBeEnabled();
await expect(docTree.getByText(topParent)).toBeVisible();
});
});
@@ -220,11 +220,11 @@ test.describe('Doc Tree', () => {
const list = page.getByTestId('doc-share-quick-search');
const currentUser = list.getByTestId(
`doc-share-member-row-user@${browserName}.test`,
`doc-share-member-row-user.test@${browserName}.test`,
);
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await currentUserRole.click();
await page.getByLabel('Administrator').click();
await page.getByRole('menuitem', { name: 'Administrator' }).click();
await list.click();
await page.getByRole('button', { name: 'Ok' }).click();
@@ -235,6 +235,12 @@ test.describe('Doc Tree', () => {
'doc-tree-detach-child',
);
await expect(
page
.getByLabel('It is the card information about the document.')
.getByText('Administrator ·'),
).toBeVisible();
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(docChild)).toBeVisible();
await docTree.click();
@@ -252,6 +258,46 @@ test.describe('Doc Tree', () => {
page.getByRole('menuitem', { name: 'Move to my docs' }),
).toHaveAttribute('aria-disabled', 'true');
});
test('keyboard navigation with Enter key opens documents', async ({
page,
browserName,
}) => {
// Create a parent document
const [docParent] = await createDoc(
page,
'doc-tree-keyboard-nav',
browserName,
1,
);
await verifyDocName(page, docParent);
// Create a sub-document
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-tree-keyboard-child',
);
const docTree = page.getByTestId('doc-tree');
await expect(docTree).toBeVisible();
// Test keyboard navigation on root document
const rootItem = page.getByTestId('doc-tree-root-item');
await expect(rootItem).toBeVisible();
// Focus on the root item and press Enter
await rootItem.focus();
await expect(rootItem).toBeFocused();
await page.keyboard.press('Enter');
// Verify we navigated to the root document
await verifyDocName(page, docParent);
await expect(page).toHaveURL(/\/docs\/[^/]+\/?$/);
// Now test keyboard navigation on sub-document
await expect(docTree.getByText(docChild)).toBeVisible();
});
});
test.describe('Doc Tree: Inheritance', () => {
@@ -18,7 +18,7 @@ test.describe('Doc Version', () => {
await verifyDocName(page, randomDoc);
await page.getByLabel('Open the document options').click();
await page.getByLabel('Version history').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await expect(page.getByText('History', { exact: true })).toBeVisible();
const modal = page.getByLabel('version history modal');
@@ -54,7 +54,7 @@ test.describe('Doc Version', () => {
).toBeVisible();
await page.getByLabel('Open the document options').click();
await page.getByLabel('Version history').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await expect(panel).toBeVisible();
await expect(page.getByText('History', { exact: true })).toBeVisible();
@@ -82,7 +82,9 @@ test.describe('Doc Version', () => {
await verifyDocName(page, 'Mocked document');
await page.getByLabel('Open the document options').click();
await expect(page.getByLabel('Version history')).toBeDisabled();
await expect(
page.getByRole('menuitem', { name: 'Version history' }),
).toBeDisabled();
});
test('it restores the doc version', async ({ page, browserName }) => {
@@ -109,7 +111,7 @@ test.describe('Doc Version', () => {
await expect(page.getByText('World')).toBeVisible();
await page.getByLabel('Open the document options').click();
await page.getByLabel('Version history').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
const modal = page.getByLabel('version history modal');
const panel = modal.getByLabel('version list');
@@ -7,6 +7,7 @@ import {
keyCloakSignIn,
verifyDocName,
} from './utils-common';
import { addNewMember, connectOtherUserToDoc } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc Visibility', () => {
@@ -44,17 +45,21 @@ test.describe('Doc Visibility', () => {
await expect(selectVisibility.getByText('Private')).toBeVisible();
await expect(page.getByLabel('Read only')).toBeHidden();
await expect(page.getByLabel('Can read and edit')).toBeHidden();
await expect(
page.getByRole('menuitem', { name: 'Read only' }),
).toBeHidden();
await expect(
page.getByRole('menuitem', { name: 'Can read and edit' }),
).toBeHidden();
await selectVisibility.click();
await page.getByLabel('Connected').click();
await page.getByRole('menuitem', { name: 'Connected' }).click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await selectVisibility.click();
await page.getByLabel('Public', { exact: true }).click();
await page.getByRole('menuitem', { name: 'Public' }).click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
});
@@ -146,47 +151,31 @@ test.describe('Doc Visibility: Restricted', () => {
await verifyDocName(page, docTitle);
await page
.locator('.ProseMirror')
.locator('.bn-block-outer')
.last()
.fill('Hello World');
const docUrl = page.url();
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
browserName,
docUrl,
});
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),
).toBeVisible({
timeout: 10000,
});
await page.getByRole('button', { name: 'Share' }).click();
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
await addNewMember(page, 0, 'Reader', otherBrowserName);
const otherBrowser = BROWSERS.find((b) => b !== browserName);
if (!otherBrowser) {
throw new Error('No alternative browser found');
}
const username = `user@${otherBrowser}.test`;
await inputSearch.fill(username);
await page.getByRole('option', { name: username }).click();
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByLabel('doc-role-dropdown').click();
await page.getByLabel('Reader').click();
await page.getByRole('button', { name: 'Invite' }).click();
await page.locator('.c__modal__backdrop').click({
position: { x: 0, y: 0 },
});
const urlDoc = page.url();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await keyCloakSignIn(page, otherBrowser);
await expect(page.getByTestId('header-logo-link')).toBeVisible();
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
await expect(page.getByLabel('Share button')).toBeVisible();
await otherPage.reload();
await expect(otherPage.getByText('Hello World')).toBeVisible();
});
});
@@ -308,7 +297,7 @@ test.describe('Doc Visibility: Public', () => {
).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Editing').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await expect(
page.getByText('The document visibility has been updated.').first(),
@@ -531,7 +520,7 @@ test.describe('Doc Visibility: Authenticated', () => {
const urlDoc = page.url();
await page.getByTestId('doc-access-mode').click();
await page.getByLabel('Editing').click();
await page.getByRole('menuitem', { name: 'Editing' }).click();
await expect(
page.getByText('The document visibility has been updated.').first(),
@@ -47,7 +47,7 @@ test.describe('Footer', () => {
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await page.getByLabel('Français').click();
await page.getByRole('menuitem', { name: 'Français' }).click();
await expect(
page.locator('footer').getByText('Mentions légales'),
@@ -132,7 +132,7 @@ test.describe('Footer', () => {
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await page.getByLabel('Français').click();
await page.getByRole('menuitem', { name: 'Français' }).click();
await expect(
page
@@ -32,6 +32,15 @@ test.describe('Header', () => {
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
header: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: 'Gouvernement Logo',
},
},
},
});
await page.goto('/');
@@ -98,6 +107,15 @@ test.describe('Header mobile', () => {
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
header: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: 'Gouvernement Logo',
},
},
},
});
await page.goto('/');
@@ -131,3 +149,27 @@ test.describe('Header: Log out', () => {
await expectLoginPage(page);
});
});
test.describe('Header: Override configuration', () => {
test('checks the header is correctly overrided', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
header: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: 'Gouvernement Logo',
},
},
},
});
await page.goto('/');
const header = page.locator('header').first();
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
await expect(header.getByAltText('Docs')).toBeHidden();
});
});
@@ -131,7 +131,7 @@ test.describe('Home page', () => {
// Keyclock login page
await expect(
page.locator('.login-pf-page-header').getByText('impress'),
page.locator('.login-pf #kc-header-wrapper').getByText('impress'),
).toBeVisible();
});
});
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test';
import { createDoc } from './utils-common';
test.describe('Left panel desktop', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -11,6 +13,53 @@ test.describe('Left panel desktop', () => {
await expect(page.getByTestId('home-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
});
test('checks resize handle is present and functional on document page', async ({
page,
browserName,
}) => {
// On home page, resize handle should NOT be present
let resizeHandle = page.locator('[data-panel-resize-handle-id]');
await expect(resizeHandle).toBeHidden();
// Create and navigate to a document
await createDoc(page, 'doc-resize-test', browserName, 1);
// Now resize handle should be visible on document page
resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
await expect(resizeHandle).toBeVisible();
const leftPanel = page.getByTestId('left-panel-desktop');
await expect(leftPanel).toBeVisible();
// Get initial panel width
const initialBox = await leftPanel.boundingBox();
expect(initialBox).not.toBeNull();
// Get handle position
const handleBox = await resizeHandle.boundingBox();
expect(handleBox).not.toBeNull();
// Test resize by dragging the handle
await page.mouse.move(
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
);
await page.mouse.down();
await page.mouse.move(
handleBox!.x + 100,
handleBox!.y + handleBox!.height / 2,
);
await page.mouse.up();
// Wait for resize to complete
await page.waitForTimeout(200);
// Verify the panel has been resized
const newBox = await leftPanel.boundingBox();
expect(newBox).not.toBeNull();
expect(newBox!.width).toBeGreaterThan(initialBox!.width);
});
});
test.describe('Left panel mobile', () => {
@@ -47,4 +96,12 @@ test.describe('Left panel mobile', () => {
await expect(languageButton).toBeInViewport();
await expect(logoutButton).toBeInViewport();
});
test('checks resize handle is not present on mobile', async ({ page }) => {
await page.goto('/');
// Verify the resize handle is NOT present on mobile
const resizeHandle = page.locator('[data-panel-resize-handle-id]');
await expect(resizeHandle).toBeHidden();
});
});
@@ -1,6 +1,7 @@
import { Page, expect } from '@playwright/test';
import { Locator, Page, expect } from '@playwright/test';
export const BROWSERS = ['chromium', 'webkit', 'firefox'];
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
export const CONFIG = {
AI_FEATURE_ENABLED: true,
@@ -22,6 +23,7 @@ export const CONFIG = {
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
SENTRY_DSN: null,
TRASHBIN_CUTOFF_DAYS: 30,
theme_customization: {},
} as const;
@@ -46,7 +48,7 @@ export const overrideConfig = async (
export const keyCloakSignIn = async (
page: Page,
browserName: string,
fromHome: boolean = true,
fromHome = true,
) => {
if (fromHome) {
await page.getByRole('button', { name: 'Start Writing' }).first().click();
@@ -56,7 +58,7 @@ export const keyCloakSignIn = async (
const password = `password-e2e-${browserName}`;
await expect(
page.locator('.login-pf-page-header').getByText('impress'),
page.locator('.login-pf #kc-header-wrapper').getByText('impress'),
).toBeVisible();
if (await page.getByLabel('Restart login').isVisible()) {
@@ -65,7 +67,7 @@ export const keyCloakSignIn = async (
await page.getByRole('textbox', { name: 'username' }).fill(login);
await page.getByRole('textbox', { name: 'password' }).fill(password);
await page.click('input[type="submit"]', { force: true });
await page.click('button[type="submit"]', { force: true });
};
export const randomName = (name: string, browserName: string, length: number) =>
@@ -77,8 +79,8 @@ export const createDoc = async (
page: Page,
docName: string,
browserName: string,
length: number = 1,
isMobile: boolean = false,
length = 1,
isMobile = false,
) => {
const randomDocs = randomName(docName, browserName, length);
@@ -322,5 +324,21 @@ export async function waitForLanguageSwitch(
await languagePicker.click();
await page.getByLabel(lang.label).click();
await page.getByRole('menuitem', { name: lang.label }).click();
}
export const clickInEditorMenu = async (page: Page, textButton: string) => {
await page.getByRole('button', { name: 'Open the document options' }).click();
await page.getByRole('menuitem', { name: textButton }).click();
};
export const clickInGridMenu = async (
page: Page,
row: Locator,
textButton: string,
) => {
await row
.getByRole('button', { name: /Open the menu of actions for the document/ })
.click();
await page.getByRole('menuitem', { name: textButton }).click();
};
@@ -0,0 +1,27 @@
import { Page } from '@playwright/test';
export const getEditor = async ({ page }: { page: Page }) => {
const editor = page.locator('.ProseMirror');
await editor.click();
return editor;
};
export const openSuggestionMenu = async ({ page }: { page: Page }) => {
const editor = await getEditor({ page });
await editor.click();
await page.locator('.bn-block-outer').last().fill('/');
return editor;
};
export const writeInEditor = async ({
page,
text,
}: {
page: Page;
text: string;
}) => {
const editor = await getEditor({ page });
await editor.locator('.bn-block-outer').last().fill(text);
return editor;
};
@@ -1,14 +1,21 @@
import { Page, expect } from '@playwright/test';
import { Page, chromium, expect } from '@playwright/test';
import {
BROWSERS,
BrowserName,
keyCloakSignIn,
verifyDocName,
} from './utils-common';
export type Role = 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader';
export type LinkReach = 'Private' | 'Connected' | 'Public';
export type LinkRole = 'Reading' | 'Edition';
export type LinkRole = 'Reading' | 'Editing';
export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
fillText: string = 'user.test',
fillText = 'user.test',
) => {
const responsePromiseSearchUser = page.waitForResponse(
(response) =>
@@ -16,9 +23,7 @@ export const addNewMember = async (
response.status() === 200,
);
const inputSearch = page.getByRole('combobox', {
name: 'Quick search input',
});
const inputSearch = page.getByTestId('quick-search-input');
// Select a new user
await inputSearch.fill(fillText);
@@ -34,7 +39,7 @@ export const addNewMember = async (
// Choose a role
await page.getByLabel('doc-role-dropdown').click();
await page.getByLabel(role).click();
await page.getByRole('menuitem', { name: role }).click();
await page.getByRole('button', { name: 'Invite' }).click();
return users[index].email;
@@ -61,6 +66,80 @@ export const updateShareLink = async (
}
};
export const updateRoleUser = async (
page: Page,
role: Role | 'Remove access',
email: string,
) => {
const list = page.getByTestId('doc-share-quick-search');
const currentUser = list.getByTestId(`doc-share-member-row-${email}`);
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
await currentUserRole.click();
await page.getByRole('menuitem', { name: role }).click();
await list.click();
};
/**
* Connects another user to a document.
* Useful to test real-time collaboration features.
* @param browserName The name of the browser to use.
* @param docUrl The URL of the document to connect to.
* @param docTitle The title of the document (optional).
* @returns An object containing the other browser, context, and page.
*/
export const connectOtherUserToDoc = async ({
browserName,
docUrl,
docTitle,
withoutSignIn,
}: {
browserName: BrowserName;
docUrl: string;
docTitle?: string;
withoutSignIn?: boolean;
}) => {
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
if (!otherBrowserName) {
throw new Error('No alternative browser found');
}
const otherBrowser = await chromium.launch({ headless: true });
const otherContext = await otherBrowser.newContext({
locale: 'en-US',
timezoneId: 'Europe/Paris',
permissions: [],
storageState: {
cookies: [],
origins: [],
},
});
const otherPage = await otherContext.newPage();
await otherPage.goto(docUrl);
if (!withoutSignIn) {
await otherPage
.getByRole('main', { name: 'Main content' })
.getByLabel('Login')
.click({
timeout: 15000,
});
await keyCloakSignIn(otherPage, otherBrowserName, false);
}
if (docTitle) {
await verifyDocName(otherPage, docTitle);
}
const cleanup = async () => {
await otherPage.close();
await otherContext.close();
await otherBrowser.close();
};
return { otherBrowser, otherContext, otherPage, otherBrowserName, cleanup };
};
export const mockedInvitations = async (page: Page, json?: object) => {
let result = [
{
@@ -72,7 +151,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
retrieve: true,
},
created_at: '2024-10-03T12:19:26.107687Z',
email: 'test@invitation.test',
email: 'test.test@invitation.test',
document: '4888c328-8406-4412-9b0b-c0ba5b9e5fb6',
role: 'editor',
issuer: '7380f42f-02eb-4ad5-b8f0-037a0e66066d',
@@ -129,7 +208,7 @@ export const mockedAccesses = async (page: Page, json?: object) => {
id: 'bc8bbbc5-a635-4f65-9817-fd1e9ec8ef87',
user: {
id: 'b4a21bb3-722e-426c-9f78-9d190eda641c',
email: 'test@accesses.test',
email: 'test.test@accesses.test',
},
team: '',
max_ancestors_role: null,
@@ -1,16 +1,18 @@
import { Page, expect } from '@playwright/test';
import {
BrowserName,
randomName,
updateDocTitle,
verifyDocName,
waitForResponseCreateDoc,
} from './utils-common';
export const createRootSubPage = async (
page: Page,
browserName: string,
browserName: BrowserName,
docName: string,
isMobile: boolean = false,
isMobile = false,
) => {
if (isMobile) {
await page
@@ -63,5 +65,64 @@ export const clickOnAddRootSubPage = async (page: Page) => {
const rootItem = page.getByTestId('doc-tree-root-item');
await expect(rootItem).toBeVisible();
await rootItem.hover();
await rootItem.getByRole('button', { name: 'add_box' }).click();
await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
};
export const addChild = async ({
page,
browserName,
docParent,
docName,
}: {
page: Page;
browserName: BrowserName;
docParent: string;
docName: string;
}) => {
let item = page.getByTestId('doc-tree-root-item');
const isParent = await item
.filter({
hasText: docParent,
})
.first()
.count();
if (!isParent) {
const items = page.getByRole('treeitem');
item = items
.filter({
hasText: docParent,
})
.first();
}
await item.hover();
await item.getByTestId('doc-tree-item-actions-add-child').click();
const [name] = randomName(docName, browserName, 1);
await updateDocTitle(page, name);
return name;
};
export const navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
await page.getByRole('link', { name: /Open root document/ }).click();
};
export const navigateToPageFromTree = async ({
page,
title,
}: {
page: Page;
title: string;
}) => {
const docTree = page.getByTestId('doc-tree');
await docTree
.getByText(title, {
exact: true,
})
.click();
await verifyDocName(page, title);
};
+6 -2
View File
@@ -1,6 +1,9 @@
{
"name": "app-e2e",
"version": "3.6.0",
"version": "3.8.21",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
"private": true,
"scripts": {
"lint": "eslint",
@@ -21,5 +24,6 @@
"dependencies": {
"convert-stream": "1.0.2",
"pdf-parse": "1.1.1"
}
},
"packageManager": "yarn@1.22.22"
}
+5
View File
@@ -35,3 +35,8 @@ yarn-error.log*
*.tsbuildinfo
service-worker.js
# Font embedding
public/assets/fonts/emoji/*
!public/assets/fonts/emoji/fallback.png
public/assets/fonts/Marianne/*
@@ -1,2 +1,3 @@
next-env.d.ts
service-worker.js
public/assets/fonts/*
@@ -0,0 +1,3 @@
out/**/*
public/assets/fonts/**/*
node_modules/**/*
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+26
View File
@@ -1,5 +1,7 @@
const crypto = require('crypto');
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
@@ -41,6 +43,30 @@ const nextConfig = {
},
);
// Copy necessary fonts from node_modules to public directory during build or dev
config.plugins.push(
new CopyPlugin({
patterns: [
{
from: path.resolve(
__dirname,
'../../node_modules/emoji-datasource-apple/img/apple/64',
),
to: path.resolve(__dirname, 'public/assets/fonts/emoji'),
force: true,
},
{
from: path.resolve(
__dirname,
'../../node_modules/@gouvfr-lasuite/ui-kit/dist/assets/fonts/Marianne',
),
to: path.resolve(__dirname, 'public/assets/fonts/Marianne'),
force: true,
},
],
}),
);
if (!isServer && process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true') {
config.plugins.push(
new InjectManifest({
+22 -13
View File
@@ -1,6 +1,9 @@
{
"name": "app-impress",
"version": "3.6.0",
"version": "3.8.21",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev",
@@ -27,43 +30,47 @@
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-outlined": "5.2.25",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.16.1",
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "3.2.3",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "10.8.0",
"@tanstack/react-query": "5.85.6",
"@sentry/nextjs": "10.11.0",
"@tanstack/react-query": "5.87.4",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.0.25",
"docx": "9.5.0",
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.4.0",
"i18next": "25.4.2",
"emoji-regex": "10.5.0",
"i18next": "25.5.2",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.1",
"next": "15.4.7",
"posthog-js": "1.261.0",
"luxon": "3.7.2",
"next": "15.5.3",
"posthog-js": "1.264.2",
"react": "*",
"react-aria-components": "1.12.1",
"react-dom": "*",
"react-i18next": "15.7.3",
"react-intersection-observer": "9.16.0",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.5",
"use-debounce": "10.0.6",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "5.0.8"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.85.6",
"@tanstack/react-query-devtools": "5.87.4",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.8.0",
"@testing-library/react": "16.3.0",
@@ -74,14 +81,15 @@
"@types/react": "*",
"@types/react-dom": "*",
"@vitejs/plugin-react": "5.0.2",
"copy-webpack-plugin": "13.0.1",
"cross-env": "10.0.0",
"dotenv": "17.2.1",
"dotenv": "17.2.2",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "26.1.0",
"node-fetch": "2.7.0",
"prettier": "3.6.2",
"stylelint": "16.23.1",
"stylelint": "16.24.0",
"stylelint-config-standard": "39.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
@@ -89,5 +97,6 @@
"vitest": "3.2.4",
"webpack": "5.101.3",
"workbox-webpack-plugin": "7.1.0"
}
},
"packageManager": "yarn@1.22.22"
}
@@ -1,101 +0,0 @@
@font-face {
font-family: Marianne;
src:
url('Marianne-Thin.woff2') format('woff2'),
url('Marianne-Thin.woff') format('woff');
font-weight: 100;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Thin_Italic.woff2') format('woff2'),
url('Marianne-Thin_Italic.woff') format('woff');
font-weight: 100;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Light.woff2') format('woff2'),
url('Marianne-Light.woff') format('woff');
font-weight: 300;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Light_Italic.woff2') format('woff2'),
url('Marianne-Light_Italic.woff') format('woff');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Regular.woff2') format('woff2'),
url('Marianne-Regular.woff') format('woff');
font-weight: 400;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Regular_Italic.woff2') format('woff2'),
url('Marianne-Regular_Italic.woff') format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Medium.woff2') format('woff2'),
url('Marianne-Medium.woff') format('woff');
font-weight: 500;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Medium_Italic.woff2') format('woff2'),
url('Marianne-Medium_Italic.woff') format('woff');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Bold.woff2') format('woff2'),
url('Marianne-Bold.woff') format('woff');
font-weight: 700;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-Bold_Italic.woff2') format('woff2'),
url('Marianne-Bold_Italic.woff') format('woff');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-ExtraBold.woff2') format('woff2'),
url('Marianne-ExtraBold.woff') format('woff');
font-weight: 800;
}
@font-face {
font-family: Marianne;
src:
url('Marianne-ExtraBold_Italic.woff2') format('woff2'),
url('Marianne-ExtraBold_Italic.woff') format('woff');
font-weight: 800;
font-style: italic;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+1 -1
View File
@@ -18,5 +18,5 @@ export const backendUrl = () =>
* @param apiVersion - The version of the API (defaults to '1.0').
* @returns The full versioned API base URL as a string.
*/
export const baseApiUrl = (apiVersion: string = '1.0') =>
export const baseApiUrl = (apiVersion = '1.0') =>
`${backendUrl()}/api/v${apiVersion}/`;
@@ -36,7 +36,30 @@ export interface BoxProps {
$position?: CSSProperties['position'];
$radius?: CSSProperties['borderRadius'];
$shrink?: CSSProperties['flexShrink'];
$theme?:
| 'primary'
| 'primary-text'
| 'secondary'
| 'secondary-text'
| 'info'
| 'success'
| 'warning'
| 'danger'
| 'greyscale';
$transition?: CSSProperties['transition'];
$variation?:
| 'text'
| '000'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| '1000';
$width?: CSSProperties['width'];
$wrap?: CSSProperties['flexWrap'];
$zIndex?: CSSProperties['zIndex'];
@@ -73,6 +96,12 @@ export const Box = styled('div')<BoxProps>`
${({ $position }) => $position && `position: ${$position};`}
${({ $radius }) => $radius && `border-radius: ${$radius};`}
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
${({ $theme, $variation }) => {
if (!$theme || !$variation) {
return '';
}
return `color: var(--c--theme--colors--${$theme}-${$variation});`;
}}
${({ $transition }) => $transition && `transition: ${$transition};`}
${({ $width }) => $width && `width: ${$width};`}
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
@@ -24,6 +24,9 @@ export type BoxButtonType = BoxType & {
*/
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
({ $css, ...props }, ref) => {
const theme = props.$theme || 'greyscale';
const variation = props.$variation || '400';
return (
<Box
ref={ref}
@@ -37,10 +40,16 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
border: none;
outline: none;
font-family: inherit;
color: ${props.disabled
? 'var(--c--theme--colors--greyscale-400) !important'
: 'inherit'};
? `var(--c--theme--colors--${theme}-400) !important`
: `inherit`};
&:focus-visible {
transition: none;
outline: 2px solid var(--c--theme--colors--${theme}-${variation});
border-radius: 1px;
outline-offset: 4px;
}
${$css || ''}
`}
{...props}
@@ -7,6 +7,7 @@ import { Box, BoxType } from '.';
export const Card = ({
children,
className,
$css,
...props
}: PropsWithChildren<BoxType>) => {
@@ -14,7 +15,7 @@ export const Card = ({
return (
<Box
className={`--docs--card ${props.className || ''}`}
className={`--docs--card ${className || ''}`}
$background="white"
$radius="4px"
$css={css`
@@ -4,21 +4,33 @@ import { css } from 'styled-components';
import { Text, TextType } from '@/components';
type IconProps = TextType & {
disabled?: boolean;
iconName: string;
variant?: 'filled' | 'outlined';
variant?: 'filled' | 'outlined' | 'symbols-outlined';
};
export const Icon = ({
className,
iconName,
disabled,
variant = 'outlined',
$variation,
...textProps
}: IconProps) => {
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
const ariaHidden =
'aria-hidden' in textProps ? textProps['aria-hidden'] : !hasLabel;
return (
<Text
{...textProps}
className={clsx('--docs--icon-bg', textProps.className, {
aria-hidden={ariaHidden}
className={clsx('--docs--icon-bg', className, {
'material-icons-filled': variant === 'filled',
'material-icons': variant === 'outlined',
'material-symbols-outlined': variant === 'symbols-outlined',
})}
$variation={disabled ? '300' : $variation}
aria-disabled={disabled}
{...textProps}
>
{iconName}
</Text>
@@ -0,0 +1,36 @@
import { PropsWithChildren } from 'react';
import { css } from 'styled-components';
import { Box, BoxType } from '.';
type OverlayerProps = PropsWithChildren<{
isOverlay: boolean;
}> &
Partial<BoxType>;
export const Overlayer = ({
children,
className,
$css,
isOverlay,
...props
}: OverlayerProps) => {
if (!isOverlay) {
return children;
}
return (
<Box
className={`--docs--overlayer ${className || ''}`}
$opacity="0.4"
$zIndex="10"
$css={css`
${$css}
pointer-events: none;
`}
{...props}
>
{children}
</Box>
);
};
@@ -13,42 +13,20 @@ export interface TextProps extends BoxProps {
$ellipsis?: boolean;
$weight?: CSSProperties['fontWeight'];
$textAlign?: CSSProperties['textAlign'];
$textTransform?: CSSProperties['textTransform'];
$size?: TextSizes | (string & {});
$theme?:
| 'primary'
| 'primary-text'
| 'secondary'
| 'secondary-text'
| 'info'
| 'success'
| 'warning'
| 'danger'
| 'greyscale';
$variation?:
| 'text'
| '000'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| '1000';
}
export type TextType = ComponentPropsWithRef<typeof Text>;
export const TextStyled = styled(Box)<TextProps>`
${({ $textAlign }) => $textAlign && `text-align: ${$textAlign};`}
${({ $textTransform }) =>
$textTransform && `text-transform: ${$textTransform};`}
${({ $weight }) => $weight && `font-weight: ${$weight};`}
${({ $size }) =>
$size &&
`font-size: ${$size in sizes ? sizes[$size as TextSizes] : $size};`}
${({ $theme, $variation }) =>
`color: var(--c--theme--colors--${$theme}-${$variation});`}
${({ $color }) => $color && `color: ${$color};`}
${({ $ellipsis }) =>
$ellipsis &&
@@ -25,8 +25,6 @@ export const TextErrors = ({
canClose = false,
...textProps
}: TextErrorsProps) => {
const { t } = useTranslation();
return (
<AlertStyled
canClose={canClose}
@@ -34,31 +32,47 @@ export const TextErrors = ({
icon={icon}
className="--docs--text-errors"
>
<Box $direction="column" $gap="0.2rem">
{causes &&
causes.map((cause, i) => (
<Text
key={`causes-${i}`}
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
{cause}
</Text>
))}
<TextOnlyErrors
causes={causes}
defaultMessage={defaultMessage}
{...textProps}
/>
</AlertStyled>
);
};
{!causes && (
export const TextOnlyErrors = ({
causes,
defaultMessage,
...textProps
}: TextErrorsProps) => {
const { t } = useTranslation();
return (
<Box $direction="column" $gap="0.2rem">
{causes &&
causes.map((cause, i) => (
<Text
key={`causes-${i}`}
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
{defaultMessage || t('Something bad happens, please retry.')}
{cause}
</Text>
)}
</Box>
</AlertStyled>
))}
{!causes && (
<Text
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
{defaultMessage || t('Something bad happens, please retry.')}
</Text>
)}
</Box>
);
};

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