Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b200ef7e3 | |||
| 81076f18fe | |||
| 0cf8b9da1a | |||
| 7be761ce84 | |||
| 5181bba083 | |||
| f434d78b5d | |||
| e07f709dd4 | |||
| afbacb0a24 | |||
| 409e073192 | |||
| 886dcb75d5 | |||
| bb4d2a9fea | |||
| 5e5054282e | |||
| f497e75426 | |||
| 97ab13ded6 | |||
| 99d674c615 | |||
| 1cdb6b62c8 | |||
| 2bf53301d2 | |||
| ec84f31bc7 | |||
| 7813219b86 | |||
| cecb4f5756 | |||
| 63efe40a7b | |||
| e26c3dff35 | |||
| f5f9d8a877 | |||
| e7709badbb | |||
| 2a7c0ef800 | |||
| 155e7dfe22 | |||
| afa48b6675 | |||
| f12d30cffa | |||
| 30dfea744a | |||
| 2cbe363a5f | |||
| 7f450e8aa8 | |||
| 7021c0f849 | |||
| e8d18d85e9 | |||
| 67a195f89c | |||
| 09b6fef63f | |||
| 11d0bafc94 | |||
| 1ae831cabd | |||
| f1c2219270 | |||
| 8c9380c356 | |||
| 3ff6d2541c | |||
| 34ce276222 | |||
| 04273c3b3e | |||
| 0b301b95c8 | |||
| 228bdf733e | |||
| b7ffc766d8 | |||
| 148890a295 | |||
| ab05fa6557 | |||
| bbf48f088f | |||
| b28ff8f632 | |||
| cdc24114b6 | |||
| 0bd53aed2c | |||
| 14b7cdf561 | |||
| c534fed196 | |||
| c1a740b7d4 | |||
| 83f2b3886e | |||
| 966e514c5a | |||
| ef6d6c6a59 | |||
| e79f3281b1 | |||
| b78550b513 | |||
| 5a23c97681 | |||
| 040eddbe6b | |||
| f2e54308d2 | |||
| cd6e0ef9e1 | |||
| 02acc7233f | |||
| 1c71e830a2 | |||
| ac0c16a44a | |||
| ca09f9a158 | |||
| d12b608db9 | |||
| 08a0eb59c8 | |||
| 0afc50fb93 | |||
| c48a4309c1 | |||
| a212417fb8 | |||
| 500d4ea5ac | |||
| 8a057b9c39 | |||
| 6a12ac560e | |||
| 2e6cb109ef | |||
| 70635136cb | |||
| 52a8dd0b5c | |||
| 8a3dfe0252 | |||
| 1110ec92d5 | |||
| 1d01f6512e | |||
| cd366213ca | |||
| d15285d385 | |||
| 377d4e8971 | |||
| 70f0c7052c | |||
| ca2e02806a | |||
| 33bd5ef116 | |||
| 7abe1c9eb4 | |||
| 95838e332c | |||
| 82f2cb59e6 | |||
| 44909faa67 | |||
| 1c5270e301 | |||
| 6af8d78ede | |||
| 304b3be273 | |||
| 17ece3b715 | |||
| 510d6c3ff1 | |||
| cab7771b82 | |||
| 93d9dec068 | |||
| adb15dedb8 | |||
| 6ece3264d6 | |||
| 2a3b31fcff | |||
| 9a64ebc1e9 | |||
| cb2ecfcea3 | |||
| 13696ffbd7 | |||
| 40ed2d2e22 | |||
| ecb20f6f77 | |||
| 7bc060988d | |||
| 122e510ff4 | |||
| f717a39109 | |||
| 04b8400766 | |||
| d232654c55 | |||
| d0eb2275e5 | |||
| 50faf766c8 | |||
| 433cead0ac | |||
| d12c637dad | |||
| 184b5c015b | |||
| 1ab237af3b | |||
| f782a0236b | |||
| c1fc1bd52f | |||
| 1c34305393 | |||
| 611ba496d2 | |||
| 0a9a583a67 | |||
| 8f67e382ba | |||
| 18d46acd75 | |||
| fae024229e | |||
| df2b953e53 | |||
| a7c91f9443 | |||
| 0a5887c162 | |||
| 26c7af0dbf | |||
| 0499aec624 | |||
| 21624e9224 | |||
| b0a9ce0938 | |||
| e256017628 | |||
| 50ce604ade | |||
| 55979e4370 | |||
| 9a8f952210 | |||
| 118804e810 | |||
| 651f2d1d75 | |||
| b96de36382 | |||
| 65b6701708 | |||
| 0be366b7b6 | |||
| 78a6772bab | |||
| fde520a6f3 | |||
| cef2d274fc | |||
| a9db392a61 | |||
| 186ae952f5 | |||
| f3c9c41b86 | |||
| 58bf5071c2 | |||
| e148c237f1 | |||
| e82e6a1fcf | |||
| fc1678d0c2 | |||
| 2b2e81f042 | |||
| c8ae2f6549 | |||
| 1d741871d7 | |||
| ddac6197e3 |
@@ -1,6 +0,0 @@
|
||||
<!---
|
||||
Thanks for filing an issue 😄 ! Before you submit, please read the following:
|
||||
|
||||
Check the other issue templates if you are trying to submit a bug report, feature request, or question
|
||||
Search open/closed issues before submitting since someone might have asked the same thing before!
|
||||
-->
|
||||
@@ -6,6 +6,10 @@ labels: ["bug", "triage"]
|
||||
|
||||
## Bug Report
|
||||
|
||||
**Before you file your issue**
|
||||
- Check the other [issues](https://github.com/suitenumerique/docs/issues) before filing your own
|
||||
- If your report is related to the ([BlockNote](https://github.com/TypeCellOS/BlockNote)) text editor, [file it on their repo](https://github.com/TypeCellOS/BlockNote/issues). If you're not sure whether your issue is with BlockNote or Docs, file it on our repo: if we support it, we'll backport it upstream ourselves 😊, otherwise we'll ask you to do so.
|
||||
|
||||
**Problematic behavior**
|
||||
A clear and concise description of the behavior.
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Set e2e env variables
|
||||
run: cat env.d/development/common.e2e.dist >> env.d/development/common.dist
|
||||
run: cat env.d/development/common.e2e >> env.d/development/common.local
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright chromium
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Set e2e env variables
|
||||
run: cat env.d/development/common.e2e.dist >> env.d/development/common.dist
|
||||
run: cat env.d/development/common.e2e >> env.d/development/common.local
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: cd src/frontend/apps/e2e && yarn install --frozen-lockfile && yarn install-playwright firefox webkit chromium
|
||||
|
||||
+1
-2
@@ -40,8 +40,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
env.d/development/*
|
||||
!env.d/development/*.dist
|
||||
env.d/development/*.local
|
||||
env.d/terraform
|
||||
|
||||
# npm
|
||||
|
||||
+126
-50
@@ -8,18 +8,96 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚡️(frontend) improve accessibility:
|
||||
- #1248
|
||||
- #1235
|
||||
- #1255
|
||||
- #1262
|
||||
- #1244
|
||||
- #1270
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(makefile) Windows compatibility fix for Docker volume mounting #1264
|
||||
- 🐛(minio) fix user permission error with Minio and Windows #1264
|
||||
|
||||
|
||||
## [3.5.0] - 2025-07-31
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add customization for translations #857
|
||||
- 📝(project) add troubleshoot doc #1066
|
||||
- 📝(project) add system-requirement doc #1066
|
||||
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
|
||||
- (doc) add documentation to install with compose #855
|
||||
- ✨(backend) allow to disable checking unsafe mimetype on attachment upload
|
||||
- ✨Ask for access #1081
|
||||
- ✨(helm) Service Account support for K8s Resources in Helm Charts #780
|
||||
- ✨(backend) allow masking documents from the list view #1172
|
||||
- ✨(frontend) subdocs can manage link reach #1190
|
||||
- ✨(frontend) add duplicate action to doc tree #1175
|
||||
- ✨(frontend) Interlinking doc #904
|
||||
- ✨(frontend) add multi columns support for editor #1219
|
||||
- ✨(api) add API route to fetch document content #1206
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(frontend) search on all docs if no children #1184
|
||||
- ♻️(frontend) redirect to doc after duplicate #1175
|
||||
- 🔧(project) change env.d system by using local files #1200
|
||||
- ⚡️(frontend) improve tree stability #1207
|
||||
- ⚡️(frontend) improve accessibility #1232
|
||||
- 🛂(frontend) block drag n drop when not desktop #1239
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(service-worker) Fix useOffline Maximum update depth exceeded #1196
|
||||
- 🐛(frontend) fix empty left panel after deleting root doc #1197
|
||||
- 🐛(helm) charts generate invalid YAML for collaboration API / WS #890
|
||||
- 🐛(frontend) 401 redirection overridden #1214
|
||||
- 🐛(frontend) include root parent in search #1243
|
||||
|
||||
## [3.4.2] - 2025-07-18
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚡️(docker) Optimize Dockerfile to use apk with --no-cache #743
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) improve prompt to not use code blocks delimiter #1188
|
||||
|
||||
## [3.4.1] - 2025-07-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🌐(frontend) keep simple tag during export #1154
|
||||
- 🐛(back) manage can-edit endpoint without created room
|
||||
in the ws #1152
|
||||
- 🐛(frontend) fix action buttons not clickable #1162
|
||||
- 🐛(frontend) fix crash share modal on grid options #1174
|
||||
- 🐛(frontend) fix unfold subdocs not clickable at the bottom #1179
|
||||
|
||||
## [3.4.0] - 2025-07-09
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) multi-pages #701
|
||||
- ✨(frontend) Duplicate a doc #1078
|
||||
- ✨Ask for access #1081
|
||||
- ✨(frontend) add customization for translations #857
|
||||
- ✨(backend) add ancestors links definitions to document abilities #846
|
||||
- ✨(backend) include ancestors accesses on document accesses list view #846
|
||||
- ✨(backend) add ancestors links reach and role to document API #846
|
||||
- 📝(project) add troubleshoot doc #1066
|
||||
- 📝(project) add system-requirement doc #1066
|
||||
- 🔧(frontend) configure x-frame-options to DENY in nginx conf #1084
|
||||
- ✨(backend) allow to disable checking unsafe mimetype on
|
||||
attachment upload #1099
|
||||
- ✨(doc) add documentation to install with compose #855
|
||||
- ✨ Give priority to users connected to collaboration server
|
||||
(aka no websocket feature) #1093
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(backend) stop requiring owner for non-root documents #846
|
||||
- ♻️(backend) simplify roles by ranking them and return only the max role #846
|
||||
- 📌(yjs) stop pinning node to minor version on yjs docker image #1005
|
||||
- 🧑💻(docker) add .next to .dockerignore #1055
|
||||
- 🧑💻(docker) handle frontend development images with docker compose #1033
|
||||
@@ -28,19 +106,18 @@ and this project adheres to
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) fix link definition select options linked to ancestors #846
|
||||
- 🐛(frontend) table of content disappearing #982
|
||||
- 🐛(frontend) fix multiple EmojiPicker #1012
|
||||
- 🐛(frontend) fix meta title #1017
|
||||
- 🔧(git) set LF line endings for all text files #1032
|
||||
- 📝(docs) minor fixes to docs/env.md
|
||||
- ✨(backend) support `_FILE` environment variables for secrets #912
|
||||
- ✨(frontend) support `_FILE` environment variables for secrets #912
|
||||
- ✨support `_FILE` environment variables for secrets #912
|
||||
|
||||
### Removed
|
||||
|
||||
- 🔥(frontend) remove Beta from logo #1095
|
||||
|
||||
|
||||
## [3.3.0] - 2025-05-06
|
||||
|
||||
### Added
|
||||
@@ -66,13 +143,13 @@ and this project adheres to
|
||||
- ⬆️(docker) upgrade node images to alpine 3.21 #973
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(y-provider) increase JSON size limits for transcription conversion #989
|
||||
|
||||
### Removed
|
||||
|
||||
- 🔥(back) remove footer endpoint #948
|
||||
|
||||
|
||||
## [3.2.1] - 2025-05-06
|
||||
|
||||
## Fixed
|
||||
@@ -80,7 +157,6 @@ and this project adheres to
|
||||
- 🐛(frontend) fix list copy paste #943
|
||||
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
|
||||
|
||||
|
||||
## [3.2.0] - 2025-05-05
|
||||
|
||||
## Added
|
||||
@@ -91,7 +167,7 @@ and this project adheres to
|
||||
- ✨(settings) Allow configuring PKCE for the SSO #886
|
||||
- 🌐(i18n) activate chinese and spanish languages #884
|
||||
- 🔧(backend) allow overwriting the data directory #893
|
||||
- ➕(backend) add `django-lasuite` dependency #839
|
||||
- ➕(backend) add `django-lasuite` dependency #839
|
||||
- ✨(frontend) advanced table features #908
|
||||
|
||||
## Changed
|
||||
@@ -142,7 +218,6 @@ and this project adheres to
|
||||
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
|
||||
- 🔒️(back) restrict access to document accesses #801
|
||||
|
||||
|
||||
## [2.6.0] - 2025-03-21
|
||||
|
||||
## Added
|
||||
@@ -161,7 +236,6 @@ and this project adheres to
|
||||
- 🔒️(back) throttle user list endpoint #636
|
||||
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
|
||||
|
||||
|
||||
## [2.5.0] - 2025-03-18
|
||||
|
||||
## Added
|
||||
@@ -184,15 +258,14 @@ and this project adheres to
|
||||
## Fixed
|
||||
|
||||
- 🐛(frontend) SVG export #706
|
||||
- 🐛(frontend) remove scroll listener table content #688
|
||||
- 🐛(frontend) remove scroll listener table content #688
|
||||
- 🔒️(back) restrict access to favorite_list endpoint #690
|
||||
- 🐛(backend) refactor to fix filtering on children
|
||||
and descendants views #695
|
||||
- 🐛(backend) refactor to fix filtering on children
|
||||
and descendants views #695
|
||||
- 🐛(action) fix notify-argocd workflow #713
|
||||
- 🚨(helm) fix helmfile lint #736
|
||||
- 🚚(frontend) redirect to 401 page when 401 error #759
|
||||
|
||||
|
||||
## [2.4.0] - 2025-03-06
|
||||
|
||||
## Added
|
||||
@@ -207,7 +280,6 @@ and this project adheres to
|
||||
|
||||
- 🐛(frontend) fix collaboration error #684
|
||||
|
||||
|
||||
## [2.3.0] - 2025-03-03
|
||||
|
||||
## Added
|
||||
@@ -623,33 +695,37 @@ and this project adheres to
|
||||
- ✨(frontend) Coming Soon page (#67)
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.3.0...main
|
||||
[v3.3.0]: https://github.com/numerique-gouv/impress/releases/v3.3.0
|
||||
[v3.2.1]: https://github.com/numerique-gouv/impress/releases/v3.2.1
|
||||
[v3.2.0]: https://github.com/numerique-gouv/impress/releases/v3.2.0
|
||||
[v3.1.0]: https://github.com/numerique-gouv/impress/releases/v3.1.0
|
||||
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
|
||||
[v2.6.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
|
||||
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0
|
||||
[v2.4.0]: https://github.com/numerique-gouv/impress/releases/v2.4.0
|
||||
[v2.3.0]: https://github.com/numerique-gouv/impress/releases/v2.3.0
|
||||
[v2.2.0]: https://github.com/numerique-gouv/impress/releases/v2.2.0
|
||||
[v2.1.0]: https://github.com/numerique-gouv/impress/releases/v2.1.0
|
||||
[v2.0.1]: https://github.com/numerique-gouv/impress/releases/v2.0.1
|
||||
[v2.0.0]: https://github.com/numerique-gouv/impress/releases/v2.0.0
|
||||
[v1.10.0]: https://github.com/numerique-gouv/impress/releases/v1.10.0
|
||||
[v1.9.0]: https://github.com/numerique-gouv/impress/releases/v1.9.0
|
||||
[v1.8.2]: https://github.com/numerique-gouv/impress/releases/v1.8.2
|
||||
[v1.8.1]: https://github.com/numerique-gouv/impress/releases/v1.8.1
|
||||
[v1.8.0]: https://github.com/numerique-gouv/impress/releases/v1.8.0
|
||||
[v1.7.0]: https://github.com/numerique-gouv/impress/releases/v1.7.0
|
||||
[v1.6.0]: https://github.com/numerique-gouv/impress/releases/v1.6.0
|
||||
[1.5.1]: https://github.com/numerique-gouv/impress/releases/v1.5.1
|
||||
[1.5.0]: https://github.com/numerique-gouv/impress/releases/v1.5.0
|
||||
[1.4.0]: https://github.com/numerique-gouv/impress/releases/v1.4.0
|
||||
[1.3.0]: https://github.com/numerique-gouv/impress/releases/v1.3.0
|
||||
[1.2.1]: https://github.com/numerique-gouv/impress/releases/v1.2.1
|
||||
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
|
||||
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
|
||||
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
|
||||
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
|
||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.5.0...main
|
||||
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
|
||||
[v3.4.2]: https://github.com/suitenumerique/docs/releases/v3.4.2
|
||||
[v3.4.1]: https://github.com/suitenumerique/docs/releases/v3.4.1
|
||||
[v3.4.0]: https://github.com/suitenumerique/docs/releases/v3.4.0
|
||||
[v3.3.0]: https://github.com/suitenumerique/docs/releases/v3.3.0
|
||||
[v3.2.1]: https://github.com/suitenumerique/docs/releases/v3.2.1
|
||||
[v3.2.0]: https://github.com/suitenumerique/docs/releases/v3.2.0
|
||||
[v3.1.0]: https://github.com/suitenumerique/docs/releases/v3.1.0
|
||||
[v3.0.0]: https://github.com/suitenumerique/docs/releases/v3.0.0
|
||||
[v2.6.0]: https://github.com/suitenumerique/docs/releases/v2.6.0
|
||||
[v2.5.0]: https://github.com/suitenumerique/docs/releases/v2.5.0
|
||||
[v2.4.0]: https://github.com/suitenumerique/docs/releases/v2.4.0
|
||||
[v2.3.0]: https://github.com/suitenumerique/docs/releases/v2.3.0
|
||||
[v2.2.0]: https://github.com/suitenumerique/docs/releases/v2.2.0
|
||||
[v2.1.0]: https://github.com/suitenumerique/docs/releases/v2.1.0
|
||||
[v2.0.1]: https://github.com/suitenumerique/docs/releases/v2.0.1
|
||||
[v2.0.0]: https://github.com/suitenumerique/docs/releases/v2.0.0
|
||||
[v1.10.0]: https://github.com/suitenumerique/docs/releases/v1.10.0
|
||||
[v1.9.0]: https://github.com/suitenumerique/docs/releases/v1.9.0
|
||||
[v1.8.2]: https://github.com/suitenumerique/docs/releases/v1.8.2
|
||||
[v1.8.1]: https://github.com/suitenumerique/docs/releases/v1.8.1
|
||||
[v1.8.0]: https://github.com/suitenumerique/docs/releases/v1.8.0
|
||||
[v1.7.0]: https://github.com/suitenumerique/docs/releases/v1.7.0
|
||||
[v1.6.0]: https://github.com/suitenumerique/docs/releases/v1.6.0
|
||||
[1.5.1]: https://github.com/suitenumerique/docs/releases/v1.5.1
|
||||
[1.5.0]: https://github.com/suitenumerique/docs/releases/v1.5.0
|
||||
[1.4.0]: https://github.com/suitenumerique/docs/releases/v1.4.0
|
||||
[1.3.0]: https://github.com/suitenumerique/docs/releases/v1.3.0
|
||||
[1.2.1]: https://github.com/suitenumerique/docs/releases/v1.2.1
|
||||
[1.2.0]: https://github.com/suitenumerique/docs/releases/v1.2.0
|
||||
[1.1.0]: https://github.com/suitenumerique/docs/releases/v1.1.0
|
||||
[1.0.0]: https://github.com/suitenumerique/docs/releases/v1.0.0
|
||||
[0.1.0]: https://github.com/suitenumerique/docs/releases/v0.1.0
|
||||
|
||||
+4
-5
@@ -7,8 +7,7 @@ FROM python:3.13.3-alpine AS base
|
||||
RUN python -m pip install --upgrade pip setuptools
|
||||
|
||||
# Upgrade system packages to install security updates
|
||||
RUN apk update && \
|
||||
apk upgrade
|
||||
RUN apk update && apk upgrade --no-cache
|
||||
|
||||
# ---- Back-end builder image ----
|
||||
FROM base AS back-builder
|
||||
@@ -45,7 +44,7 @@ FROM base AS link-collector
|
||||
ARG IMPRESS_STATIC_ROOT=/data/static
|
||||
|
||||
# Install pango & rdfind
|
||||
RUN apk add \
|
||||
RUN apk add --no-cache \
|
||||
pango \
|
||||
rdfind
|
||||
|
||||
@@ -71,7 +70,7 @@ FROM base AS core
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Install required system libs
|
||||
RUN apk add \
|
||||
RUN apk add --no-cache \
|
||||
cairo \
|
||||
file \
|
||||
font-noto \
|
||||
@@ -117,7 +116,7 @@ FROM core AS backend-development
|
||||
USER root:root
|
||||
|
||||
# Install psql
|
||||
RUN apk add postgresql-client
|
||||
RUN apk add --no-cache postgresql-client
|
||||
|
||||
# Uninstall impress and re-install it in editable mode along with development
|
||||
# dependencies
|
||||
|
||||
@@ -35,9 +35,13 @@ DB_PORT = 5432
|
||||
|
||||
# -- Docker
|
||||
# Get the current user ID to use for docker run and docker exec commands
|
||||
DOCKER_UID = $(shell id -u)
|
||||
DOCKER_GID = $(shell id -g)
|
||||
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
|
||||
ifeq ($(OS),Windows_NT)
|
||||
DOCKER_USER := 0:0 # run containers as root on Windows
|
||||
else
|
||||
DOCKER_UID := $(shell id -u)
|
||||
DOCKER_GID := $(shell id -g)
|
||||
DOCKER_USER := $(DOCKER_UID):$(DOCKER_GID)
|
||||
endif
|
||||
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
|
||||
COMPOSE_E2E = DOCKER_USER=$(DOCKER_USER) docker compose -f compose.yml -f compose-e2e.yml
|
||||
COMPOSE_EXEC = $(COMPOSE) exec
|
||||
@@ -48,7 +52,7 @@ COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
|
||||
|
||||
# -- Backend
|
||||
MANAGE = $(COMPOSE_RUN_APP) python manage.py
|
||||
MAIL_YARN = $(COMPOSE_RUN) -w /app/src/mail node yarn
|
||||
MAIL_YARN = $(COMPOSE_RUN) -w //app/src/mail node yarn
|
||||
|
||||
# -- Frontend
|
||||
PATH_FRONT = ./src/frontend
|
||||
@@ -67,18 +71,18 @@ data/static:
|
||||
|
||||
# -- Project
|
||||
|
||||
create-env-files: ## Copy the dist env files to env files
|
||||
create-env-files: \
|
||||
env.d/development/common \
|
||||
env.d/development/crowdin \
|
||||
env.d/development/postgresql \
|
||||
env.d/development/kc_postgresql
|
||||
.PHONY: create-env-files
|
||||
create-env-local-files: ## create env.local files in env.d/development
|
||||
create-env-local-files:
|
||||
@touch env.d/development/crowdin.local
|
||||
@touch env.d/development/common.local
|
||||
@touch env.d/development/postgresql.local
|
||||
@touch env.d/development/kc_postgresql.local
|
||||
.PHONY: create-env-local-files
|
||||
|
||||
pre-bootstrap: \
|
||||
data/media \
|
||||
data/static \
|
||||
create-env-files
|
||||
create-env-local-files
|
||||
.PHONY: pre-bootstrap
|
||||
|
||||
post-bootstrap: \
|
||||
@@ -258,20 +262,6 @@ resetdb: ## flush database and create a superuser "admin"
|
||||
@${MAKE} superuser
|
||||
.PHONY: resetdb
|
||||
|
||||
env.d/development/common:
|
||||
cp -n env.d/development/common.dist env.d/development/common
|
||||
|
||||
env.d/development/postgresql:
|
||||
cp -n env.d/development/postgresql.dist env.d/development/postgresql
|
||||
|
||||
env.d/development/kc_postgresql:
|
||||
cp -n env.d/development/kc_postgresql.dist env.d/development/kc_postgresql
|
||||
|
||||
# -- Internationalization
|
||||
|
||||
env.d/development/crowdin:
|
||||
cp -n env.d/development/crowdin.dist env.d/development/crowdin
|
||||
|
||||
crowdin-download: ## Download translated message from crowdin
|
||||
@$(COMPOSE_RUN_CROWDIN) download -c crowdin/config.yml
|
||||
.PHONY: crowdin-download
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
web: bin/buildpack_start.sh
|
||||
postdeploy: python manage.py migrate
|
||||
@@ -34,8 +34,6 @@ Docs, where your notes can become knowledge through live collaboration.
|
||||
## Why use Docs ❓
|
||||
Docs is a collaborative text editor designed to address common challenges in knowledge building and sharing.
|
||||
|
||||
It offers a scalable and secure alternative to tools such as Google Docs, Notion (without the dbs), Outline, or Confluence.
|
||||
|
||||
### Write
|
||||
* 😌 Get simple, accessible online editing for your team.
|
||||
* 💅 Create clean documents with beautiful formatting options.
|
||||
|
||||
@@ -38,6 +38,10 @@ function _set_user() {
|
||||
# options: docker compose command options
|
||||
# ARGS : docker compose command arguments
|
||||
function _docker_compose() {
|
||||
# Set DOCKER_USER for Windows compatibility with MinIO
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || -n "${WSL_DISTRO_NAME:-}" ]]; then
|
||||
export DOCKER_USER="0:0"
|
||||
fi
|
||||
|
||||
echo "🐳(compose) file: '${COMPOSE_FILE}'"
|
||||
docker compose \
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-compile script"
|
||||
|
||||
rm -rf docker docs env.d gitlint src/frontend/apps/e2e
|
||||
rm -rf src/frontend/apps
|
||||
rm -rf src/frontend/packages
|
||||
|
||||
# Remove some of the larger packages required by the frontend only
|
||||
rm -rf src/frontend/node_modules/@next src/frontend/node_modules/next src/frontend/node_modules/react-icons src/frontend/node_modules/@gouvfr-lasuite
|
||||
|
||||
# du -ch | sort -rh | head -n 100
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-frontend script"
|
||||
|
||||
# Move the frontend build to the nginx root and clean up
|
||||
mkdir -p build/
|
||||
mv src/frontend/apps/impress/out build/frontend-out
|
||||
|
||||
mv src/backend/* ./
|
||||
mv src/nginx/* ./
|
||||
|
||||
echo "3.13" > .python-version
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start the Django backend server
|
||||
gunicorn -b :8000 impress.wsgi:application --log-file - &
|
||||
|
||||
# Start the Y provider service
|
||||
cd src/frontend/servers/y-provider && PORT=4444 ../../.scalingo/node/bin/node dist/start-server.js &
|
||||
|
||||
# Start the Nginx server
|
||||
bin/run &
|
||||
|
||||
# if the current shell is killed, also terminate all its children
|
||||
trap "pkill SIGTERM -P $$" SIGTERM
|
||||
|
||||
# wait for a single child to finish,
|
||||
wait -n
|
||||
# then kill all the other tasks
|
||||
pkill -P $$
|
||||
@@ -24,5 +24,6 @@ services:
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/common.local
|
||||
ports:
|
||||
- "4444:4444"
|
||||
@@ -10,6 +10,7 @@ services:
|
||||
retries: 300
|
||||
env_file:
|
||||
- env.d/development/postgresql
|
||||
- env.d/development/postgresql.local
|
||||
ports:
|
||||
- "15432:5432"
|
||||
|
||||
@@ -66,7 +67,9 @@ services:
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/common.local
|
||||
- env.d/development/postgresql
|
||||
- env.d/development/postgresql.local
|
||||
ports:
|
||||
- "8071:8000"
|
||||
volumes:
|
||||
@@ -91,7 +94,9 @@ services:
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/common.local
|
||||
- env.d/development/postgresql
|
||||
- env.d/development/postgresql.local
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
@@ -135,6 +140,7 @@ services:
|
||||
- ".:/app"
|
||||
env_file:
|
||||
- env.d/development/crowdin
|
||||
- env.d/development/crowdin.local
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
working_dir: /app
|
||||
|
||||
@@ -156,6 +162,7 @@ services:
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- env.d/development/common
|
||||
- env.d/development/common.local
|
||||
ports:
|
||||
- "4444:4444"
|
||||
volumes:
|
||||
@@ -174,6 +181,7 @@ services:
|
||||
- "5433:5432"
|
||||
env_file:
|
||||
- env.d/development/kc_postgresql
|
||||
- env.d/development/kc_postgresql.local
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:20.0.1
|
||||
|
||||
+101
-100
@@ -6,104 +6,104 @@ Here we describe all environment variables that can be set for the docs applicat
|
||||
|
||||
These are the environment variables you can set for the `impress-backend` container.
|
||||
|
||||
| Option | Description | default |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
||||
| AI_API_KEY | AI key to be used for AI Base url | |
|
||||
| AI_BASE_URL | OpenAI compatible AI base url | |
|
||||
| AI_FEATURE_ENABLED | Enable AI options | false |
|
||||
| AI_MODEL | AI Model to use | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
|
||||
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
|
||||
| API_USERS_LIST_THROTTLE_RATE_BURST | throttle rate for api on burst | 30/minute |
|
||||
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | throttle rate for api | 180/hour |
|
||||
| AWS_S3_ACCESS_KEY_ID | access id for s3 endpoint | |
|
||||
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
|
||||
| AWS_S3_REGION_NAME | region name for s3 endpoint | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | access key for s3 endpoint | |
|
||||
| AWS_STORAGE_BUCKET_NAME | bucket name for s3 endpoint | impress-media-storage |
|
||||
| CACHES_DEFAULT_TIMEOUT | cache default timeout | 30 |
|
||||
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
|
||||
| COLLABORATION_API_URL | collaboration api host | |
|
||||
| COLLABORATION_SERVER_SECRET | collaboration api secret | |
|
||||
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
|
||||
| COLLABORATION_WS_URL | collaboration websocket url | |
|
||||
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
|
||||
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert-markdown |
|
||||
| CONVERSION_API_SECURE | Require secure conversion api | false |
|
||||
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
|
||||
| CONTENT_SECURITY_POLICY_DIRECTIVES | A dict of directives set in the Content-Security-Policy header | All directives are set to 'none' |
|
||||
| CONTENT_SECURITY_POLICY_EXCLUDE_URL_PREFIXES | Url with this prefix will not have the header Content-Security-Policy included | |
|
||||
| CRISP_WEBSITE_ID | crisp website id for support | |
|
||||
| DB_ENGINE | engine to use for database connections | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_HOST | host of the database | localhost |
|
||||
| DB_NAME | name of the database | impress |
|
||||
| DB_PASSWORD | password to authenticate with | pass |
|
||||
| DB_PORT | port of the database | 5432 |
|
||||
| DB_USER | user to authenticate with | dinum |
|
||||
| DJANGO_ALLOWED_HOSTS | allowed hosts | [] |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker transport options | {} |
|
||||
| DJANGO_CELERY_BROKER_URL | celery broker url | redis://redis:6379/0 |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | list of origins allowed for CORS using regulair expressions | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | list of origins allowed for CORS | [] |
|
||||
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
|
||||
| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_BRAND_NAME | brand name for email | |
|
||||
| DJANGO_EMAIL_FROM | email address used as sender | from@example.com |
|
||||
| DJANGO_EMAIL_HOST | host name of email | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | password to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_HOST_USER | user to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_LOGO_IMG | logo for the email | |
|
||||
| DJANGO_EMAIL_PORT | port used to connect to email host | |
|
||||
| DJANGO_EMAIL_USE_SSL | use sstl for email host connection | false |
|
||||
| DJANGO_EMAIL_USE_TLS | use tls for email host connection | false |
|
||||
| DJANGO_SECRET_KEY | secret key | |
|
||||
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
|
||||
| DOCUMENT_IMAGE_MAX_SIZE | maximum size of document in bytes | 10485760 |
|
||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_THEME | frontend theme to use | |
|
||||
| LANGUAGE_CODE | default language | en-us |
|
||||
| LOGGING_LEVEL_LOGGERS_APP | application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGGING_LEVEL_LOGGERS_ROOT | default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGIN_REDIRECT_URL | login redirect url | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | login redirect url on failure | |
|
||||
| LOGOUT_REDIRECT_URL | logout redirect url | |
|
||||
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
|
||||
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
||||
| MEDIA_BASE_URL | | |
|
||||
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
||||
| OIDC_CREATE_USER | create used on OIDC | false |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | faillback to email for identification | true |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
|
||||
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
|
||||
| OIDC_RP_CLIENT_ID | client id used for OIDC | impress |
|
||||
| OIDC_RP_CLIENT_SECRET | client secret used for OIDC | |
|
||||
| OIDC_RP_SCOPES | scopes requested for OIDC | openid email |
|
||||
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
|
||||
| OIDC_USE_NONCE | use nonce for OIDC | true |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
|
||||
| POSTHOG_KEY | posthog key for analytics | |
|
||||
| REDIS_URL | cache url | redis://redis:6379/1 |
|
||||
| SENTRY_DSN | sentry host | |
|
||||
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
|
||||
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
|
||||
| THEME_CUSTOMIZATION_FILE_PATH | full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
|
||||
| TRASHBIN_CUTOFF_DAYS | trashbin cutoff | 30 |
|
||||
| USER_OIDC_ESSENTIAL_CLAIMS | essential claims in OIDC token | [] |
|
||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||
| Option | Description | default |
|
||||
|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
||||
| AI_API_KEY | AI key to be used for AI Base url | |
|
||||
| AI_BASE_URL | OpenAI compatible AI base url | |
|
||||
| AI_FEATURE_ENABLED | Enable AI options | false |
|
||||
| AI_MODEL | AI Model to use | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
|
||||
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
|
||||
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
|
||||
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
|
||||
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
|
||||
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
|
||||
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
|
||||
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
|
||||
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
|
||||
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
|
||||
| COLLABORATION_API_URL | Collaboration api host | |
|
||||
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
|
||||
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
|
||||
| COLLABORATION_WS_URL | Collaboration websocket url | |
|
||||
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
|
||||
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert |
|
||||
| CONVERSION_API_SECURE | Require secure conversion api | false |
|
||||
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
|
||||
| CRISP_WEBSITE_ID | Crisp website id for support | |
|
||||
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_HOST | Host of the database | localhost |
|
||||
| DB_NAME | Name of the database | impress |
|
||||
| DB_PASSWORD | Password to authenticate with | pass |
|
||||
| DB_PORT | Port of the database | 5432 |
|
||||
| DB_USER | User to authenticate with | dinum |
|
||||
| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker url | redis://redis:6379/0 |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | List of origins allowed for CORS using regulair expressions | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | List of origins allowed for CORS | [] |
|
||||
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_BRAND_NAME | Brand name for email | |
|
||||
| DJANGO_EMAIL_FROM | Email address used as sender | from@example.com |
|
||||
| DJANGO_EMAIL_HOST | Hostname of email | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
|
||||
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
|
||||
| DJANGO_EMAIL_PORT | Port used to connect to email host | |
|
||||
| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false |
|
||||
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
|
||||
| DJANGO_SECRET_KEY | Secret key | |
|
||||
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
|
||||
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
|
||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||
| FRONTEND_THEME | Frontend theme to use | |
|
||||
| LANGUAGE_CODE | Default language | en-us |
|
||||
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||
| LOGIN_REDIRECT_URL | Login redirect url | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect url on failure | |
|
||||
| LOGOUT_REDIRECT_URL | Logout redirect url | |
|
||||
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
|
||||
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
||||
| MEDIA_BASE_URL | | |
|
||||
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
|
||||
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
||||
| OIDC_CREATE_USER | Create used on OIDC | false |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | true |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
|
||||
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
|
||||
| OIDC_RP_CLIENT_ID | Client id used for OIDC | impress |
|
||||
| OIDC_RP_CLIENT_SECRET | Client secret used for OIDC | |
|
||||
| OIDC_RP_SCOPES | Scopes requested for OIDC | openid email |
|
||||
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
|
||||
| POSTHOG_KEY | Posthog key for analytics | |
|
||||
| REDIS_URL | Cache url | redis://redis:6379/1 |
|
||||
| SENTRY_DSN | Sentry host | |
|
||||
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
|
||||
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
|
||||
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
|
||||
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
|
||||
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
|
||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||
|
||||
|
||||
## impress-frontend image
|
||||
|
||||
@@ -136,9 +136,10 @@ NODE_ENV=production NEXT_PUBLIC_PUBLISH_AS_MIT=false yarn build
|
||||
|
||||
Packages with licences incompatible with the MIT licence:
|
||||
* `xl-docx-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-docx-exporter/LICENSE),
|
||||
* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE)
|
||||
* `xl-pdf-exporter`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE),
|
||||
* `xl-multi-column`: [AGPL-3.0](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-multi-column/LICENSE).
|
||||
|
||||
In `.env.development`, `PUBLISH_AS_MIT` is set to `false`, allowing developers to test Docs with all its features.
|
||||
|
||||
⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your [BlockNote licensing](https://github.com/TypeCellOS/BlockNote/blob/main/packages/xl-pdf-exporter/LICENSE) or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.
|
||||
⚠️ If you run Docs in production with `PUBLISH_AS_MIT` set to `false` make sure you fulfill your BlockNote licensing or [subscription](https://www.blocknotejs.org/about#partner-with-us) obligations.
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
```bash
|
||||
mkdir keycloak
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
|
||||
curl -o env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
|
||||
curl -o env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
|
||||
curl -o keycloak/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
|
||||
curl -o keycloak/env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/kc_postgresql
|
||||
curl -o keycloak/env.d/keycloak https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/keycloak
|
||||
```
|
||||
|
||||
### Step 2:. Update `env.d/` files
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
```bash
|
||||
mkdir minio
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
|
||||
curl -o minio/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/minio/compose.yaml
|
||||
```
|
||||
|
||||
### Step 2:. Update compose file with your own values
|
||||
|
||||
@@ -13,7 +13,7 @@ Acme-companion is a lightweight companion container for nginx-proxy. It handles
|
||||
|
||||
```bash
|
||||
mkdir nginx-proxy
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
|
||||
curl -o nginx-proxy/compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
|
||||
```
|
||||
|
||||
### Step 2: Edit `DEFAULT_EMAIL` in the compose file.
|
||||
|
||||
@@ -46,9 +46,6 @@ backend:
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
AWS_S3_ENDPOINT_URL: http://minio.impress.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: root
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Installation with docker compose
|
||||
|
||||
We provide a sample configuration for running Docs using Docker Compose. Please note that this configuration is experimental, and the official way to deploy Docs in production is to use [k8s](../installation/k8s.md)
|
||||
We provide a sample configuration for running Docs using Docker Compose. Please note that this configuration is experimental, and the official way to deploy Docs in production is to use [k8s](../installation/kubernetes.md)
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -31,11 +31,17 @@ For older versions of Docker Engine that do not include Docker Compose:
|
||||
|
||||
```bash
|
||||
mkdir -p docs/env.d
|
||||
cd docs
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docs/examples/compose/compose.yaml
|
||||
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/common
|
||||
curl -o env.d/backend https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/backend
|
||||
curl -o env.d/yprovider https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/yprovider
|
||||
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
|
||||
curl -o env.d/postgresql https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/env.d/production.dist/postgresql
|
||||
```
|
||||
|
||||
If you are using the sample nginx-proxy configuration:
|
||||
```bash
|
||||
curl -o default.conf.template https://raw.githubusercontent.com/suitenumerique/docs/refs/heads/main/docker/files/production/etc/nginx/conf.d/default.conf.template
|
||||
```
|
||||
|
||||
## Step 2: Configuration
|
||||
|
||||
@@ -168,9 +168,6 @@ DB_NAME: impress
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
```
|
||||
|
||||
### Find s3 bucket connection values
|
||||
|
||||
@@ -83,55 +83,6 @@ If you already have CRLF line endings in your local repository, the **best appro
|
||||
git commit -m "✏️(project) Fix line endings to LF"
|
||||
```
|
||||
|
||||
## Minio Permission Issues on Windows
|
||||
|
||||
### Problem Description
|
||||
|
||||
On Windows, you may encounter permission-related errors when running Minio in development mode with Docker Compose. This typically happens because:
|
||||
|
||||
- **Windows file permissions** don't map well to Unix-style user IDs used in Docker containers
|
||||
- **Docker Desktop** may have issues with user mapping when using the `DOCKER_USER` environment variable
|
||||
- **Minio container** fails to start or access volumes due to permission conflicts
|
||||
|
||||
### Common Symptoms
|
||||
|
||||
- Minio container fails to start with permission denied errors
|
||||
- Error messages related to file system permissions in Minio logs
|
||||
- Unable to create or access buckets in the development environment
|
||||
- Docker Compose showing Minio service as unhealthy or exited
|
||||
|
||||
### Solution for Windows Users
|
||||
|
||||
If you encounter Minio permission issues on Windows, you can temporarily disable user mapping for the Minio service:
|
||||
|
||||
1. **Open the `compose.yml` file**
|
||||
|
||||
2. **Comment out the user directive** in the `minio` service section:
|
||||
```yaml
|
||||
minio:
|
||||
# user: ${DOCKER_USER:-1000} # Comment this line on Windows if permission issues occur
|
||||
image: minio/minio
|
||||
environment:
|
||||
- MINIO_ROOT_USER=impress
|
||||
- MINIO_ROOT_PASSWORD=password
|
||||
# ... rest of the configuration
|
||||
```
|
||||
|
||||
3. **Restart the services**:
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
- Commenting out the `user` directive allows the Minio container to run with its default user
|
||||
- This bypasses Windows-specific permission mapping issues
|
||||
- The container will have the necessary permissions to access and manage the mounted volumes
|
||||
|
||||
### Note
|
||||
|
||||
This is a **development-only workaround**. In production environments, proper user mapping and security considerations should be maintained according to your deployment requirements.
|
||||
|
||||
## Frontend File Watching Issues on Windows
|
||||
|
||||
### Problem Description
|
||||
|
||||
@@ -60,6 +60,7 @@ COLLABORATION_API_URL=http://y-provider-development:4444/collaboration/api/
|
||||
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
|
||||
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
|
||||
COLLABORATION_SERVER_SECRET=my-secret
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=true
|
||||
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
|
||||
|
||||
DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
|
||||
@@ -2,4 +2,4 @@
|
||||
BURST_THROTTLE_RATES="200/minute"
|
||||
COLLABORATION_API_URL=http://y-provider:4444/collaboration/api/
|
||||
SUSTAINED_THROTTLE_RATES="200/hour"
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
@@ -1,4 +1,4 @@
|
||||
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api
|
||||
Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api/
|
||||
Y_PROVIDER_API_KEY=<generate a random key>
|
||||
COLLABORATION_SERVER_SECRET=<generate a random key>
|
||||
COLLABORATION_SERVER_ORIGIN=https://${DOCS_HOST}
|
||||
|
||||
@@ -60,6 +60,9 @@ class ListDocumentFilter(DocumentFilter):
|
||||
is_creator_me = django_filters.BooleanFilter(
|
||||
method="filter_is_creator_me", label=_("Creator is me")
|
||||
)
|
||||
is_masked = django_filters.BooleanFilter(
|
||||
method="filter_is_masked", label=_("Masked")
|
||||
)
|
||||
is_favorite = django_filters.BooleanFilter(
|
||||
method="filter_is_favorite", label=_("Favorite")
|
||||
)
|
||||
@@ -106,3 +109,22 @@ class ListDocumentFilter(DocumentFilter):
|
||||
return queryset
|
||||
|
||||
return queryset.filter(is_favorite=bool(value))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def filter_is_masked(self, queryset, name, value):
|
||||
"""
|
||||
Filter documents based on whether they are masked by the current user.
|
||||
|
||||
Example:
|
||||
- /api/v1.0/documents/?is_masked=true
|
||||
→ Filters documents marked as masked by the logged-in user
|
||||
- /api/v1.0/documents/?is_masked=false
|
||||
→ Filters documents not marked as masked by the logged-in user
|
||||
"""
|
||||
user = self.request.user
|
||||
|
||||
if not user.is_authenticated:
|
||||
return queryset
|
||||
|
||||
queryset_method = queryset.filter if bool(value) else queryset.exclude
|
||||
return queryset_method(link_traces__user=user, link_traces__is_masked=True)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.http import Http404
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
from core import choices
|
||||
from core.models import DocumentAccess, RoleChoices, get_trashbin_cutoff
|
||||
|
||||
ACTION_FOR_METHOD_TO_PERMISSION = {
|
||||
@@ -96,26 +97,27 @@ class CanCreateInvitationPermission(permissions.BasePermission):
|
||||
).exists()
|
||||
|
||||
|
||||
class AccessPermission(permissions.BasePermission):
|
||||
"""Permission class for access objects."""
|
||||
class ResourceWithAccessPermission(permissions.BasePermission):
|
||||
"""A permission class for templates and invitations."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""check create permission for templates."""
|
||||
return request.user.is_authenticated or view.action != "create"
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""Check permission for a given object."""
|
||||
abilities = obj.get_abilities(request.user)
|
||||
action = view.action
|
||||
try:
|
||||
action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method]
|
||||
except KeyError:
|
||||
pass
|
||||
return abilities.get(action, False)
|
||||
|
||||
|
||||
class DocumentAccessPermission(AccessPermission):
|
||||
class DocumentPermission(permissions.BasePermission):
|
||||
"""Subclass to handle soft deletion specificities."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""check create permission for documents."""
|
||||
return request.user.is_authenticated or view.action != "create"
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""
|
||||
Return a 404 on deleted documents
|
||||
@@ -127,10 +129,45 @@ class DocumentAccessPermission(AccessPermission):
|
||||
) and deleted_at < get_trashbin_cutoff():
|
||||
raise Http404
|
||||
|
||||
# Compute permission first to ensure the "user_roles" attribute is set
|
||||
has_permission = super().has_object_permission(request, view, obj)
|
||||
abilities = obj.get_abilities(request.user)
|
||||
action = view.action
|
||||
try:
|
||||
action = ACTION_FOR_METHOD_TO_PERMISSION[view.action][request.method]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
has_permission = abilities.get(action, False)
|
||||
|
||||
if obj.ancestors_deleted_at and not RoleChoices.OWNER in obj.user_roles:
|
||||
raise Http404
|
||||
|
||||
return has_permission
|
||||
|
||||
|
||||
class ResourceAccessPermission(IsAuthenticated):
|
||||
"""Permission class for document access objects."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""check create permission for accesses in documents tree."""
|
||||
if super().has_permission(request, view) is False:
|
||||
return False
|
||||
|
||||
if view.action == "create":
|
||||
role = getattr(view, view.resource_field_name).get_role(request.user)
|
||||
if role not in choices.PRIVILEGED_ROLES:
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to manage accesses for this resource."
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""Check permission for a given object."""
|
||||
abilities = obj.get_abilities(request.user)
|
||||
|
||||
requested_role = request.data.get("role")
|
||||
if requested_role and requested_role not in abilities.get("set_role_to", []):
|
||||
return False
|
||||
|
||||
action = view.action
|
||||
return abilities.get(action, False)
|
||||
|
||||
+161
-140
@@ -10,9 +10,9 @@ from django.utils.functional import lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import magic
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework import serializers
|
||||
|
||||
from core import enums, models, utils
|
||||
from core import choices, enums, models, utils
|
||||
from core.services.ai_services import AI_ACTIONS
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
@@ -32,134 +32,35 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
class UserLightSerializer(UserSerializer):
|
||||
"""Serialize users with limited fields."""
|
||||
|
||||
id = serializers.SerializerMethodField(read_only=True)
|
||||
email = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def get_id(self, _user):
|
||||
"""Return always None. Here to have the same fields than in UserSerializer."""
|
||||
return None
|
||||
|
||||
def get_email(self, _user):
|
||||
"""Return always None. Here to have the same fields than in UserSerializer."""
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "email", "full_name", "short_name"]
|
||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
||||
fields = ["full_name", "short_name"]
|
||||
read_only_fields = ["full_name", "short_name"]
|
||||
|
||||
|
||||
class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
class TemplateAccessSerializer(serializers.ModelSerializer):
|
||||
"""Serialize template accesses."""
|
||||
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Make "user" field is readonly but only on update."""
|
||||
validated_data.pop("user", None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def get_abilities(self, access) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return access.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
Check access rights specific to writing (create/update)
|
||||
"""
|
||||
request = self.context.get("request")
|
||||
user = getattr(request, "user", None)
|
||||
role = attrs.get("role")
|
||||
|
||||
# Update
|
||||
if self.instance:
|
||||
can_set_role_to = self.instance.get_abilities(user)["set_role_to"]
|
||||
|
||||
if role and role not in can_set_role_to:
|
||||
message = (
|
||||
f"You are only allowed to set role to {', '.join(can_set_role_to)}"
|
||||
if can_set_role_to
|
||||
else "You are not allowed to set this role for this template."
|
||||
)
|
||||
raise exceptions.PermissionDenied(message)
|
||||
|
||||
# Create
|
||||
else:
|
||||
try:
|
||||
resource_id = self.context["resource_id"]
|
||||
except KeyError as exc:
|
||||
raise exceptions.ValidationError(
|
||||
"You must set a resource ID in kwargs to create a new access."
|
||||
) from exc
|
||||
|
||||
if not self.Meta.model.objects.filter( # pylint: disable=no-member
|
||||
Q(user=user) | Q(team__in=user.teams),
|
||||
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
|
||||
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
||||
).exists():
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to manage accesses for this resource."
|
||||
)
|
||||
|
||||
if (
|
||||
role == models.RoleChoices.OWNER
|
||||
and not self.Meta.model.objects.filter( # pylint: disable=no-member
|
||||
Q(user=user) | Q(team__in=user.teams),
|
||||
role=models.RoleChoices.OWNER,
|
||||
**{self.Meta.resource_field_name: resource_id}, # pylint: disable=no-member
|
||||
).exists()
|
||||
):
|
||||
raise exceptions.PermissionDenied(
|
||||
"Only owners of a resource can assign other users as owners."
|
||||
)
|
||||
|
||||
# pylint: disable=no-member
|
||||
attrs[f"{self.Meta.resource_field_name}_id"] = self.context["resource_id"]
|
||||
return attrs
|
||||
|
||||
|
||||
class DocumentAccessSerializer(BaseAccessSerializer):
|
||||
"""Serialize document accesses."""
|
||||
|
||||
user_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=models.User.objects.all(),
|
||||
write_only=True,
|
||||
source="user",
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
user = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = ["id", "user", "user_id", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "abilities"]
|
||||
|
||||
|
||||
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
||||
"""Serialize document accesses with limited fields."""
|
||||
|
||||
user = UserLightSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
fields = ["id", "user", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "team", "role", "abilities"]
|
||||
|
||||
|
||||
class TemplateAccessSerializer(BaseAccessSerializer):
|
||||
"""Serialize template accesses."""
|
||||
|
||||
class Meta:
|
||||
model = models.TemplateAccess
|
||||
resource_field_name = "template"
|
||||
fields = ["id", "user", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "abilities"]
|
||||
|
||||
def get_abilities(self, instance) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return instance.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Make "user" field is readonly but only on update."""
|
||||
validated_data.pop("user", None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"""Serialize documents with limited fields for display in lists."""
|
||||
@@ -167,7 +68,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
nb_accesses_ancestors = serializers.IntegerField(read_only=True)
|
||||
nb_accesses_direct = serializers.IntegerField(read_only=True)
|
||||
user_roles = serializers.SerializerMethodField(read_only=True)
|
||||
user_role = serializers.SerializerMethodField(read_only=True)
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -175,6 +76,10 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
@@ -188,11 +93,15 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"path",
|
||||
"title",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
@@ -205,46 +114,62 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"numchild",
|
||||
"path",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
]
|
||||
|
||||
def get_abilities(self, document) -> dict:
|
||||
def to_representation(self, instance):
|
||||
"""Precompute once per instance"""
|
||||
paths_links_mapping = self.context.get("paths_links_mapping")
|
||||
|
||||
if paths_links_mapping is not None:
|
||||
links = paths_links_mapping.get(instance.path[: -instance.steplen], [])
|
||||
instance.ancestors_link_definition = choices.get_equivalent_link_definition(
|
||||
links
|
||||
)
|
||||
|
||||
return super().to_representation(instance)
|
||||
|
||||
def get_abilities(self, instance) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if not request:
|
||||
return {}
|
||||
|
||||
if request:
|
||||
paths_links_mapping = self.context.get("paths_links_mapping", None)
|
||||
# Retrieve ancestor links from paths_links_mapping (if provided)
|
||||
ancestors_links = (
|
||||
paths_links_mapping.get(document.path[: -document.steplen])
|
||||
if paths_links_mapping
|
||||
else None
|
||||
)
|
||||
return document.get_abilities(request.user, ancestors_links=ancestors_links)
|
||||
return instance.get_abilities(request.user)
|
||||
|
||||
return {}
|
||||
|
||||
def get_user_roles(self, document):
|
||||
def get_user_role(self, instance):
|
||||
"""
|
||||
Return roles of the logged-in user for the current document,
|
||||
taking into account ancestors.
|
||||
"""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return document.get_roles(request.user)
|
||||
return []
|
||||
return instance.get_role(request.user) if request else None
|
||||
|
||||
|
||||
class DocumentLightSerializer(serializers.ModelSerializer):
|
||||
"""Minial document serializer for nesting in document accesses."""
|
||||
|
||||
class Meta:
|
||||
model = models.Document
|
||||
fields = ["id", "path", "depth"]
|
||||
read_only_fields = ["id", "path", "depth"]
|
||||
|
||||
|
||||
class DocumentSerializer(ListDocumentSerializer):
|
||||
"""Serialize documents with all fields for display in detail views."""
|
||||
|
||||
content = serializers.CharField(required=False)
|
||||
websocket = serializers.BooleanField(required=False, write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Document
|
||||
fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"content",
|
||||
"created_at",
|
||||
"creator",
|
||||
@@ -259,11 +184,16 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"path",
|
||||
"title",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
"websocket",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"abilities",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
@@ -275,7 +205,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"numchild",
|
||||
"path",
|
||||
"updated_at",
|
||||
"user_roles",
|
||||
"user_role",
|
||||
]
|
||||
|
||||
def get_fields(self):
|
||||
@@ -361,6 +291,99 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
return super().save(**kwargs)
|
||||
|
||||
|
||||
class DocumentAccessSerializer(serializers.ModelSerializer):
|
||||
"""Serialize document accesses."""
|
||||
|
||||
document = DocumentLightSerializer(read_only=True)
|
||||
user_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=models.User.objects.all(),
|
||||
write_only=True,
|
||||
source="user",
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
user = UserSerializer(read_only=True)
|
||||
team = serializers.CharField(required=False, allow_blank=True)
|
||||
abilities = serializers.SerializerMethodField(read_only=True)
|
||||
max_ancestors_role = serializers.SerializerMethodField(read_only=True)
|
||||
max_role = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = [
|
||||
"id",
|
||||
"document",
|
||||
"user",
|
||||
"user_id",
|
||||
"team",
|
||||
"role",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"document",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
|
||||
def get_abilities(self, instance) -> dict:
|
||||
"""Return abilities of the logged-in user on the instance."""
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return instance.get_abilities(request.user)
|
||||
return {}
|
||||
|
||||
def get_max_ancestors_role(self, instance):
|
||||
"""Return max_ancestors_role if annotated; else None."""
|
||||
return getattr(instance, "max_ancestors_role", None)
|
||||
|
||||
def get_max_role(self, instance):
|
||||
"""Return max_ancestors_role if annotated; else None."""
|
||||
return choices.RoleChoices.max(
|
||||
getattr(instance, "max_ancestors_role", None),
|
||||
instance.role,
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Make "user" field readonly but only on update."""
|
||||
validated_data.pop("team", None)
|
||||
validated_data.pop("user", None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class DocumentAccessLightSerializer(DocumentAccessSerializer):
|
||||
"""Serialize document accesses with limited fields."""
|
||||
|
||||
user = UserLightSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = [
|
||||
"id",
|
||||
"document",
|
||||
"user",
|
||||
"team",
|
||||
"role",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"document",
|
||||
"team",
|
||||
"role",
|
||||
"abilities",
|
||||
"max_ancestors_role",
|
||||
"max_role",
|
||||
]
|
||||
|
||||
|
||||
class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for creating a document from a server-to-server request.
|
||||
@@ -408,9 +431,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
language = user.language or language
|
||||
|
||||
try:
|
||||
document_content = YdocConverter().convert_markdown(
|
||||
validated_data["content"]
|
||||
)
|
||||
document_content = YdocConverter().convert(validated_data["content"])
|
||||
except ConversionError as err:
|
||||
raise serializers.ValidationError(
|
||||
{"content": ["Could not convert content"]}
|
||||
|
||||
+482
-257
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
"""Declare and configure choices for Docs' core application."""
|
||||
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class PriorityTextChoices(TextChoices):
|
||||
"""
|
||||
This class inherits from Django's TextChoices and provides a method to get the priority
|
||||
of a given value based on its position in the class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_priority(cls, role):
|
||||
"""Returns the priority of the given role based on its order in the class."""
|
||||
|
||||
members = list(cls.__members__.values())
|
||||
return members.index(role) + 1 if role in members else 0
|
||||
|
||||
@classmethod
|
||||
def max(cls, *roles):
|
||||
"""
|
||||
Return the highest-priority role among the given roles, using get_priority().
|
||||
If no valid roles are provided, returns None.
|
||||
"""
|
||||
valid_roles = [role for role in roles if cls.get_priority(role) is not None]
|
||||
if not valid_roles:
|
||||
return None
|
||||
return max(valid_roles, key=cls.get_priority)
|
||||
|
||||
|
||||
class LinkRoleChoices(PriorityTextChoices):
|
||||
"""Defines the possible roles a link can offer on a document."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
|
||||
|
||||
class RoleChoices(PriorityTextChoices):
|
||||
"""Defines the possible roles a user can have in a resource."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
|
||||
OWNER = "owner", _("Owner")
|
||||
|
||||
|
||||
PRIVILEGED_ROLES = [RoleChoices.ADMIN, RoleChoices.OWNER]
|
||||
|
||||
|
||||
class LinkReachChoices(PriorityTextChoices):
|
||||
"""Defines types of access for links"""
|
||||
|
||||
RESTRICTED = (
|
||||
"restricted",
|
||||
_("Restricted"),
|
||||
) # Only users with a specific access can read/edit the document
|
||||
AUTHENTICATED = (
|
||||
"authenticated",
|
||||
_("Authenticated"),
|
||||
) # Any authenticated user can access the document
|
||||
PUBLIC = "public", _("Public") # Even anonymous users can access the document
|
||||
|
||||
@classmethod
|
||||
def get_select_options(cls, link_reach, link_role):
|
||||
"""
|
||||
Determines the valid select options for link reach and link role depending on the
|
||||
ancestors' link reach/role given as arguments.
|
||||
Returns:
|
||||
Dictionary mapping possible reach levels to their corresponding possible roles.
|
||||
"""
|
||||
return {
|
||||
reach: [
|
||||
role
|
||||
for role in LinkRoleChoices.values
|
||||
if LinkRoleChoices.get_priority(role)
|
||||
>= LinkRoleChoices.get_priority(link_role)
|
||||
]
|
||||
if reach != cls.RESTRICTED
|
||||
else None
|
||||
for reach in cls.values
|
||||
if LinkReachChoices.get_priority(reach)
|
||||
>= LinkReachChoices.get_priority(link_reach)
|
||||
}
|
||||
|
||||
|
||||
def get_equivalent_link_definition(ancestors_links):
|
||||
"""
|
||||
Return the (reach, role) pair with:
|
||||
1. Highest reach
|
||||
2. Highest role among links having that reach
|
||||
"""
|
||||
if not ancestors_links:
|
||||
return {"link_reach": None, "link_role": None}
|
||||
|
||||
# 1) Find the highest reach
|
||||
max_reach = max(
|
||||
ancestors_links,
|
||||
key=lambda link: LinkReachChoices.get_priority(link["link_reach"]),
|
||||
)["link_reach"]
|
||||
|
||||
# 2) Among those, find the highest role (ignore role if RESTRICTED)
|
||||
if max_reach == LinkReachChoices.RESTRICTED:
|
||||
max_role = None
|
||||
else:
|
||||
max_role = max(
|
||||
(
|
||||
link["link_role"]
|
||||
for link in ancestors_links
|
||||
if link["link_reach"] == max_reach
|
||||
),
|
||||
key=LinkRoleChoices.get_priority,
|
||||
)
|
||||
|
||||
return {"link_reach": max_reach, "link_role": max_role}
|
||||
@@ -150,7 +150,7 @@ class DocumentFactory(factory.django.DjangoModelFactory):
|
||||
"""Add link traces to document from a given list of users."""
|
||||
if create and extracted:
|
||||
for item in extracted:
|
||||
models.LinkTrace.objects.create(document=self, user=item)
|
||||
models.LinkTrace.objects.update_or_create(document=self, user=item)
|
||||
|
||||
@factory.post_generation
|
||||
def favorited_by(self, create, extracted, **kwargs):
|
||||
@@ -159,6 +159,15 @@ class DocumentFactory(factory.django.DjangoModelFactory):
|
||||
for item in extracted:
|
||||
models.DocumentFavorite.objects.create(document=self, user=item)
|
||||
|
||||
@factory.post_generation
|
||||
def masked_by(self, create, extracted, **kwargs):
|
||||
"""Mark document as masked by a list of users."""
|
||||
if create and extracted:
|
||||
for item in extracted:
|
||||
models.LinkTrace.objects.update_or_create(
|
||||
document=self, user=item, defaults={"is_masked": True}
|
||||
)
|
||||
|
||||
|
||||
class UserDocumentAccessFactory(factory.django.DjangoModelFactory):
|
||||
"""Create fake document user accesses for testing."""
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Force session creation for all requests."""
|
||||
|
||||
|
||||
class ForceSessionMiddleware:
|
||||
"""
|
||||
Force session creation for unauthenticated users.
|
||||
Must be used after Authentication middleware.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
"""Initialize the middleware."""
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
"""Force session creation for unauthenticated users."""
|
||||
|
||||
if not request.user.is_authenticated and request.session.session_key is None:
|
||||
request.session.create()
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-14 14:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0022_alter_user_language_documentaskforaccess"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="document",
|
||||
name="has_deleted_children",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-13 08:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0023_remove_document_is_public_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="linktrace",
|
||||
name="is_masked",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="language",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("en-us", "English"),
|
||||
("fr-fr", "Français"),
|
||||
("de-de", "Deutsch"),
|
||||
("nl-nl", "Nederlands"),
|
||||
("es-es", "Español"),
|
||||
],
|
||||
default=None,
|
||||
help_text="The language in which the user wants to see the interface.",
|
||||
max_length=10,
|
||||
null=True,
|
||||
verbose_name="language",
|
||||
),
|
||||
),
|
||||
]
|
||||
+310
-234
@@ -6,7 +6,6 @@ Declare and configure the models for the impress core application
|
||||
import hashlib
|
||||
import smtplib
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from logging import getLogger
|
||||
|
||||
@@ -33,6 +32,14 @@ from rest_framework.exceptions import ValidationError
|
||||
from timezone_field import TimeZoneField
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager, MP_NodeQuerySet
|
||||
|
||||
from .choices import (
|
||||
PRIVILEGED_ROLES,
|
||||
LinkReachChoices,
|
||||
LinkRoleChoices,
|
||||
RoleChoices,
|
||||
get_equivalent_link_definition,
|
||||
)
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@@ -50,88 +57,6 @@ def get_trashbin_cutoff():
|
||||
return timezone.now() - timedelta(days=settings.TRASHBIN_CUTOFF_DAYS)
|
||||
|
||||
|
||||
class LinkRoleChoices(models.TextChoices):
|
||||
"""Defines the possible roles a link can offer on a document."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
|
||||
|
||||
class RoleChoices(models.TextChoices):
|
||||
"""Defines the possible roles a user can have in a resource."""
|
||||
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
|
||||
OWNER = "owner", _("Owner")
|
||||
|
||||
|
||||
PRIVILEGED_ROLES = [RoleChoices.ADMIN, RoleChoices.OWNER]
|
||||
|
||||
|
||||
class LinkReachChoices(models.TextChoices):
|
||||
"""Defines types of access for links"""
|
||||
|
||||
RESTRICTED = (
|
||||
"restricted",
|
||||
_("Restricted"),
|
||||
) # Only users with a specific access can read/edit the document
|
||||
AUTHENTICATED = (
|
||||
"authenticated",
|
||||
_("Authenticated"),
|
||||
) # Any authenticated user can access the document
|
||||
PUBLIC = "public", _("Public") # Even anonymous users can access the document
|
||||
|
||||
@classmethod
|
||||
def get_select_options(cls, ancestors_links):
|
||||
"""
|
||||
Determines the valid select options for link reach and link role depending on the
|
||||
list of ancestors' link reach/role.
|
||||
|
||||
Args:
|
||||
ancestors_links: List of dictionaries, each with 'link_reach' and 'link_role' keys
|
||||
representing the reach and role of ancestors links.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping possible reach levels to their corresponding possible roles.
|
||||
"""
|
||||
# If no ancestors, return all options
|
||||
if not ancestors_links:
|
||||
return dict.fromkeys(cls.values, LinkRoleChoices.values)
|
||||
|
||||
# Initialize result with all possible reaches and role options as sets
|
||||
result = {reach: set(LinkRoleChoices.values) for reach in cls.values}
|
||||
|
||||
# Group roles by reach level
|
||||
reach_roles = defaultdict(set)
|
||||
for link in ancestors_links:
|
||||
reach_roles[link["link_reach"]].add(link["link_role"])
|
||||
|
||||
# Apply constraints based on ancestor links
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.RESTRICTED]:
|
||||
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
|
||||
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.AUTHENTICATED]:
|
||||
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
|
||||
result.pop(cls.RESTRICTED, None)
|
||||
elif LinkRoleChoices.READER in reach_roles[cls.AUTHENTICATED]:
|
||||
result[cls.RESTRICTED].discard(LinkRoleChoices.READER)
|
||||
|
||||
if LinkRoleChoices.EDITOR in reach_roles[cls.PUBLIC]:
|
||||
result[cls.PUBLIC].discard(LinkRoleChoices.READER)
|
||||
result.pop(cls.AUTHENTICATED, None)
|
||||
result.pop(cls.RESTRICTED, None)
|
||||
elif LinkRoleChoices.READER in reach_roles[cls.PUBLIC]:
|
||||
result[cls.AUTHENTICATED].discard(LinkRoleChoices.READER)
|
||||
result.get(cls.RESTRICTED, set()).discard(LinkRoleChoices.READER)
|
||||
|
||||
# Convert roles sets to lists while maintaining the order from LinkRoleChoices
|
||||
for reach, roles in result.items():
|
||||
result[reach] = [role for role in LinkRoleChoices.values if role in roles]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class DuplicateEmailError(Exception):
|
||||
"""Raised when an email is already associated with a pre-existing user."""
|
||||
|
||||
@@ -364,69 +289,6 @@ class BaseAccess(BaseModel):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def _get_roles(self, resource, user):
|
||||
"""
|
||||
Get the roles a user has on a resource.
|
||||
"""
|
||||
roles = []
|
||||
if user.is_authenticated:
|
||||
teams = user.teams
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
except AttributeError:
|
||||
try:
|
||||
roles = resource.accesses.filter(
|
||||
models.Q(user=user) | models.Q(team__in=teams),
|
||||
).values_list("role", flat=True)
|
||||
except (self._meta.model.DoesNotExist, IndexError):
|
||||
roles = []
|
||||
|
||||
return roles
|
||||
|
||||
def _get_abilities(self, resource, user):
|
||||
"""
|
||||
Compute and return abilities for a given user taking into account
|
||||
the current state of the object.
|
||||
"""
|
||||
roles = self._get_roles(resource, user)
|
||||
|
||||
is_owner_or_admin = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
if self.role == RoleChoices.OWNER:
|
||||
can_delete = (
|
||||
RoleChoices.OWNER in roles
|
||||
and resource.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
||||
)
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
)
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if RoleChoices.OWNER in roles:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
set_role_to.remove(self.role)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to),
|
||||
"partial_update": bool(set_role_to),
|
||||
"retrieve": bool(roles),
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
|
||||
|
||||
class DocumentQuerySet(MP_NodeQuerySet):
|
||||
"""
|
||||
@@ -452,6 +314,41 @@ class DocumentQuerySet(MP_NodeQuerySet):
|
||||
|
||||
return self.filter(link_reach=LinkReachChoices.PUBLIC)
|
||||
|
||||
def annotate_is_favorite(self, user):
|
||||
"""
|
||||
Annotate document queryset with the favorite status for the current user.
|
||||
"""
|
||||
if user.is_authenticated:
|
||||
favorite_exists_subquery = DocumentFavorite.objects.filter(
|
||||
document_id=models.OuterRef("pk"), user=user
|
||||
)
|
||||
return self.annotate(is_favorite=models.Exists(favorite_exists_subquery))
|
||||
|
||||
return self.annotate(is_favorite=models.Value(False))
|
||||
|
||||
def annotate_user_roles(self, user):
|
||||
"""
|
||||
Annotate document queryset with the roles of the current user
|
||||
on the document or its ancestors.
|
||||
"""
|
||||
output_field = ArrayField(base_field=models.CharField())
|
||||
|
||||
if user.is_authenticated:
|
||||
user_roles_subquery = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__path=Left(models.OuterRef("path"), Length("document__path")),
|
||||
).values_list("role", flat=True)
|
||||
|
||||
return self.annotate(
|
||||
user_roles=models.Func(
|
||||
user_roles_subquery, function="ARRAY", output_field=output_field
|
||||
)
|
||||
)
|
||||
|
||||
return self.annotate(
|
||||
user_roles=models.Value([], output_field=output_field),
|
||||
)
|
||||
|
||||
|
||||
class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
|
||||
"""
|
||||
@@ -464,6 +361,7 @@ class DocumentManager(MP_NodeManager.from_queryset(DocumentQuerySet)):
|
||||
return self._queryset_class(self.model).order_by("path")
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class Document(MP_Node, BaseModel):
|
||||
"""Pad document carrying the content."""
|
||||
|
||||
@@ -486,6 +384,7 @@ class Document(MP_Node, BaseModel):
|
||||
)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
ancestors_deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
has_deleted_children = models.BooleanField(default=False)
|
||||
duplicated_from = models.ForeignKey(
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -531,6 +430,12 @@ class Document(MP_Node, BaseModel):
|
||||
def __str__(self):
|
||||
return str(self.title) if self.title else str(_("Untitled Document"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize cache property."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._ancestors_link_definition = None
|
||||
self._computed_link_definition = None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Write content to object storage only if _content has changed."""
|
||||
super().save(*args, **kwargs)
|
||||
@@ -561,6 +466,12 @@ class Document(MP_Node, BaseModel):
|
||||
content_file = ContentFile(bytes_content)
|
||||
default_storage.save(file_key, content_file)
|
||||
|
||||
def is_leaf(self):
|
||||
"""
|
||||
:returns: True if the node is has no children
|
||||
"""
|
||||
return not self.has_deleted_children and self.numchild == 0
|
||||
|
||||
@property
|
||||
def key_base(self):
|
||||
"""Key base of the location where the document is stored in object storage."""
|
||||
@@ -718,38 +629,22 @@ class Document(MP_Node, BaseModel):
|
||||
cache_key = document.get_nb_accesses_cache_key()
|
||||
cache.delete(cache_key)
|
||||
|
||||
def get_roles(self, user):
|
||||
def get_role(self, user):
|
||||
"""Return the roles a user has on a document."""
|
||||
if not user.is_authenticated:
|
||||
return []
|
||||
return None
|
||||
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
except AttributeError:
|
||||
try:
|
||||
roles = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__path=Left(
|
||||
models.Value(self.path), Length("document__path")
|
||||
),
|
||||
).values_list("role", flat=True)
|
||||
except (models.ObjectDoesNotExist, IndexError):
|
||||
roles = []
|
||||
return roles
|
||||
roles = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__path=Left(models.Value(self.path), Length("document__path")),
|
||||
).values_list("role", flat=True)
|
||||
|
||||
def get_links_definitions(self, ancestors_links):
|
||||
"""Get links reach/role definitions for the current document and its ancestors."""
|
||||
return RoleChoices.max(*roles)
|
||||
|
||||
links_definitions = defaultdict(set)
|
||||
links_definitions[self.link_reach].add(self.link_role)
|
||||
|
||||
# Merge ancestor link definitions
|
||||
for ancestor in ancestors_links:
|
||||
links_definitions[ancestor["link_reach"]].add(ancestor["link_role"])
|
||||
|
||||
return dict(links_definitions) # Convert default dict back to a normal dict
|
||||
|
||||
def compute_ancestors_links(self, user):
|
||||
def compute_ancestors_links_paths_mapping(self):
|
||||
"""
|
||||
Compute the ancestors links for the current document up to the highest readable ancestor.
|
||||
"""
|
||||
@@ -758,63 +653,114 @@ class Document(MP_Node, BaseModel):
|
||||
.filter(ancestors_deleted_at__isnull=True)
|
||||
.order_by("path")
|
||||
)
|
||||
highest_readable = ancestors.readable_per_se(user).only("depth").first()
|
||||
|
||||
if highest_readable is None:
|
||||
return []
|
||||
|
||||
ancestors_links = []
|
||||
paths_links_mapping = {}
|
||||
for ancestor in ancestors.filter(depth__gte=highest_readable.depth):
|
||||
|
||||
for ancestor in ancestors:
|
||||
ancestors_links.append(
|
||||
{"link_reach": ancestor.link_reach, "link_role": ancestor.link_role}
|
||||
)
|
||||
paths_links_mapping[ancestor.path] = ancestors_links.copy()
|
||||
|
||||
ancestors_links = paths_links_mapping.get(self.path[: -self.steplen], [])
|
||||
return paths_links_mapping
|
||||
|
||||
return ancestors_links
|
||||
@property
|
||||
def link_definition(self):
|
||||
"""Returns link reach/role as a definition in dictionary format."""
|
||||
return {"link_reach": self.link_reach, "link_role": self.link_role}
|
||||
|
||||
def get_abilities(self, user, ancestors_links=None):
|
||||
@property
|
||||
def ancestors_link_definition(self):
|
||||
"""Link definition equivalent to all document's ancestors."""
|
||||
if getattr(self, "_ancestors_link_definition", None) is None:
|
||||
if self.depth <= 1:
|
||||
ancestors_links = []
|
||||
else:
|
||||
mapping = self.compute_ancestors_links_paths_mapping()
|
||||
ancestors_links = mapping.get(self.path[: -self.steplen], [])
|
||||
self._ancestors_link_definition = get_equivalent_link_definition(
|
||||
ancestors_links
|
||||
)
|
||||
|
||||
return self._ancestors_link_definition
|
||||
|
||||
@ancestors_link_definition.setter
|
||||
def ancestors_link_definition(self, definition):
|
||||
"""Cache the ancestors_link_definition."""
|
||||
self._ancestors_link_definition = definition
|
||||
|
||||
@property
|
||||
def ancestors_link_reach(self):
|
||||
"""Link reach equivalent to all document's ancestors."""
|
||||
return self.ancestors_link_definition["link_reach"]
|
||||
|
||||
@property
|
||||
def ancestors_link_role(self):
|
||||
"""Link role equivalent to all document's ancestors."""
|
||||
return self.ancestors_link_definition["link_role"]
|
||||
|
||||
@property
|
||||
def computed_link_definition(self):
|
||||
"""
|
||||
Link reach/role on the document, combining inherited ancestors' link
|
||||
definitions and the document's own link definition.
|
||||
"""
|
||||
if getattr(self, "_computed_link_definition", None) is None:
|
||||
self._computed_link_definition = get_equivalent_link_definition(
|
||||
[self.ancestors_link_definition, self.link_definition]
|
||||
)
|
||||
return self._computed_link_definition
|
||||
|
||||
@property
|
||||
def computed_link_reach(self):
|
||||
"""Actual link reach on the document."""
|
||||
return self.computed_link_definition["link_reach"]
|
||||
|
||||
@property
|
||||
def computed_link_role(self):
|
||||
"""Actual link role on the document."""
|
||||
return self.computed_link_definition["link_role"]
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the document.
|
||||
"""
|
||||
if self.depth <= 1 or getattr(self, "is_highest_ancestor_for_user", False):
|
||||
ancestors_links = []
|
||||
elif ancestors_links is None:
|
||||
ancestors_links = self.compute_ancestors_links(user=user)
|
||||
|
||||
roles = set(
|
||||
self.get_roles(user)
|
||||
) # at this point only roles based on specific access
|
||||
# First get the role based on specific access
|
||||
role = self.get_role(user)
|
||||
|
||||
# Characteristics that are based only on specific access
|
||||
is_owner = RoleChoices.OWNER in roles
|
||||
is_owner = role == RoleChoices.OWNER
|
||||
is_deleted = self.ancestors_deleted_at and not is_owner
|
||||
is_owner_or_admin = (is_owner or RoleChoices.ADMIN in roles) and not is_deleted
|
||||
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
|
||||
# want anonymous users to access versions (we wouldn't know from
|
||||
# which date to allow them anyway)
|
||||
# Anonymous users should also not see document accesses
|
||||
has_access_role = bool(roles) and not is_deleted
|
||||
has_access_role = bool(role) and not is_deleted
|
||||
can_update_from_access = (
|
||||
is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||
is_owner_or_admin or role == RoleChoices.EDITOR
|
||||
) and not is_deleted
|
||||
|
||||
# Add roles provided by the document link, taking into account its ancestors
|
||||
links_definitions = self.get_links_definitions(ancestors_links)
|
||||
public_roles = links_definitions.get(LinkReachChoices.PUBLIC, set())
|
||||
authenticated_roles = (
|
||||
links_definitions.get(LinkReachChoices.AUTHENTICATED, set())
|
||||
if user.is_authenticated
|
||||
else set()
|
||||
link_select_options = LinkReachChoices.get_select_options(
|
||||
**self.ancestors_link_definition
|
||||
)
|
||||
link_definition = get_equivalent_link_definition(
|
||||
[
|
||||
self.ancestors_link_definition,
|
||||
{"link_reach": self.link_reach, "link_role": self.link_role},
|
||||
]
|
||||
)
|
||||
roles = roles | public_roles | authenticated_roles
|
||||
|
||||
can_get = bool(roles) and not is_deleted
|
||||
link_reach = link_definition["link_reach"]
|
||||
if link_reach == LinkReachChoices.PUBLIC or (
|
||||
link_reach == LinkReachChoices.AUTHENTICATED and user.is_authenticated
|
||||
):
|
||||
role = RoleChoices.max(role, link_definition["link_role"])
|
||||
|
||||
can_get = bool(role) and not is_deleted
|
||||
can_update = (
|
||||
is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||
is_owner_or_admin or role == RoleChoices.EDITOR
|
||||
) and not is_deleted
|
||||
|
||||
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
|
||||
@@ -836,22 +782,25 @@ class Document(MP_Node, BaseModel):
|
||||
"ai_translate": ai_access,
|
||||
"attachment_upload": can_update,
|
||||
"media_check": can_get,
|
||||
"can_edit": can_update,
|
||||
"children_list": can_get,
|
||||
"children_create": can_update and user.is_authenticated,
|
||||
"collaboration_auth": can_get,
|
||||
"content": can_get,
|
||||
"cors_proxy": can_get,
|
||||
"descendants": can_get,
|
||||
"destroy": is_owner,
|
||||
"duplicate": can_get,
|
||||
"duplicate": can_get and user.is_authenticated,
|
||||
"favorite": can_get and user.is_authenticated,
|
||||
"link_configuration": is_owner_or_admin,
|
||||
"invite_owner": is_owner,
|
||||
"mask": can_get and user.is_authenticated,
|
||||
"move": is_owner_or_admin and not self.ancestors_deleted_at,
|
||||
"partial_update": can_update,
|
||||
"restore": is_owner,
|
||||
"retrieve": can_get,
|
||||
"media_auth": can_get,
|
||||
"link_select_options": LinkReachChoices.get_select_options(ancestors_links),
|
||||
"link_select_options": link_select_options,
|
||||
"tree": can_get,
|
||||
"update": can_update,
|
||||
"versions_destroy": is_owner_or_admin,
|
||||
@@ -946,7 +895,8 @@ class Document(MP_Node, BaseModel):
|
||||
|
||||
if self.depth > 1:
|
||||
self._meta.model.objects.filter(pk=self.get_parent().pk).update(
|
||||
numchild=models.F("numchild") - 1
|
||||
numchild=models.F("numchild") - 1,
|
||||
has_deleted_children=True,
|
||||
)
|
||||
|
||||
# Mark all descendants as soft deleted
|
||||
@@ -1010,6 +960,7 @@ class LinkTrace(BaseModel):
|
||||
related_name="link_traces",
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="link_traces")
|
||||
is_masked = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
db_table = "impress_link_trace"
|
||||
@@ -1103,48 +1054,120 @@ class DocumentAccess(BaseAccess):
|
||||
super().save(*args, **kwargs)
|
||||
self.document.invalidate_nb_accesses_cache()
|
||||
|
||||
@property
|
||||
def target_key(self):
|
||||
"""Get a unique key for the actor targeted by the access, without possible conflict."""
|
||||
return f"user:{self.user_id!s}" if self.user_id else f"team:{self.team:s}"
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Override delete to clear the document's cache for number of accesses."""
|
||||
super().delete(*args, **kwargs)
|
||||
self.document.invalidate_nb_accesses_cache()
|
||||
|
||||
def set_user_roles_tuple(self, ancestors_role, current_role):
|
||||
"""
|
||||
Set a precomputed (ancestor_role, current_role) tuple for this instance.
|
||||
|
||||
This avoids querying the database in `get_roles_tuple()` and is useful
|
||||
when roles are already known, such as in bulk serialization.
|
||||
|
||||
Args:
|
||||
ancestor_role (str | None): Highest role on any ancestor document.
|
||||
current_role (str | None): Role on the current document.
|
||||
"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self._prefetched_user_roles_tuple = (ancestors_role, current_role)
|
||||
|
||||
def get_user_roles_tuple(self, user):
|
||||
"""
|
||||
Return a tuple of:
|
||||
- the highest role the user has on any ancestor of the document
|
||||
- the role the user has on the current document
|
||||
|
||||
If roles have been explicitly set using `set_user_roles_tuple()`,
|
||||
those will be returned instead of querying the database.
|
||||
|
||||
This allows viewsets or serializers to precompute roles for performance
|
||||
when handling multiple documents at once.
|
||||
|
||||
Args:
|
||||
user (User): The user whose roles are being evaluated.
|
||||
|
||||
Returns:
|
||||
tuple[str | None, str | None]: (max_ancestor_role, current_document_role)
|
||||
"""
|
||||
if not user.is_authenticated:
|
||||
return None, None
|
||||
|
||||
try:
|
||||
return self._prefetched_user_roles_tuple
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
ancestors = (
|
||||
self.document.get_ancestors() | Document.objects.filter(pk=self.document_id)
|
||||
).filter(ancestors_deleted_at__isnull=True)
|
||||
|
||||
access_tuples = DocumentAccess.objects.filter(
|
||||
models.Q(user=user) | models.Q(team__in=user.teams),
|
||||
document__in=ancestors,
|
||||
).values_list("document_id", "role")
|
||||
|
||||
ancestors_roles = []
|
||||
current_roles = []
|
||||
for doc_id, role in access_tuples:
|
||||
if doc_id == self.document_id:
|
||||
current_roles.append(role)
|
||||
else:
|
||||
ancestors_roles.append(role)
|
||||
|
||||
return RoleChoices.max(*ancestors_roles), RoleChoices.max(*current_roles)
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the document access.
|
||||
"""
|
||||
roles = self._get_roles(self.document, user)
|
||||
is_owner_or_admin = bool(set(roles).intersection(set(PRIVILEGED_ROLES)))
|
||||
ancestors_role, current_role = self.get_user_roles_tuple(user)
|
||||
role = RoleChoices.max(ancestors_role, current_role)
|
||||
is_owner_or_admin = role in PRIVILEGED_ROLES
|
||||
|
||||
if self.role == RoleChoices.OWNER:
|
||||
can_delete = (
|
||||
RoleChoices.OWNER in roles
|
||||
and self.document.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
||||
)
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
can_delete = role == RoleChoices.OWNER and (
|
||||
# check if document is not root trying to avoid an extra query
|
||||
self.document.depth > 1
|
||||
or DocumentAccess.objects.filter(
|
||||
document_id=self.document_id, role=RoleChoices.OWNER
|
||||
).count()
|
||||
> 1
|
||||
)
|
||||
set_role_to = RoleChoices.values if can_delete else []
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if RoleChoices.OWNER in roles:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
[RoleChoices.READER, RoleChoices.EDITOR, RoleChoices.ADMIN]
|
||||
)
|
||||
if role == RoleChoices.OWNER:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
set_role_to.remove(self.role)
|
||||
except ValueError:
|
||||
pass
|
||||
# Filter out roles that would be lower than the one the user already has
|
||||
ancestors_role_priority = RoleChoices.get_priority(
|
||||
getattr(self, "max_ancestors_role", None)
|
||||
)
|
||||
set_role_to = [
|
||||
candidate_role
|
||||
for candidate_role in set_role_to
|
||||
if RoleChoices.get_priority(candidate_role) >= ancestors_role_priority
|
||||
]
|
||||
if len(set_role_to) == 1:
|
||||
set_role_to = []
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to) and is_owner_or_admin,
|
||||
"partial_update": bool(set_role_to) and is_owner_or_admin,
|
||||
"retrieve": self.user and self.user.id == user.id or is_owner_or_admin,
|
||||
"retrieve": (self.user and self.user.id == user.id) or is_owner_or_admin,
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
|
||||
@@ -1277,10 +1300,10 @@ class Template(BaseModel):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_roles(self, user):
|
||||
def get_role(self, user):
|
||||
"""Return the roles a user has on a resource as an iterable."""
|
||||
if not user.is_authenticated:
|
||||
return []
|
||||
return None
|
||||
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
@@ -1291,21 +1314,20 @@ class Template(BaseModel):
|
||||
).values_list("role", flat=True)
|
||||
except (models.ObjectDoesNotExist, IndexError):
|
||||
roles = []
|
||||
return roles
|
||||
|
||||
return RoleChoices.max(*roles)
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the template.
|
||||
"""
|
||||
roles = self.get_roles(user)
|
||||
is_owner_or_admin = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
can_get = self.is_public or bool(roles)
|
||||
can_update = is_owner_or_admin or RoleChoices.EDITOR in roles
|
||||
role = self.get_role(user)
|
||||
is_owner_or_admin = role in PRIVILEGED_ROLES
|
||||
can_get = self.is_public or bool(role)
|
||||
can_update = is_owner_or_admin or role == RoleChoices.EDITOR
|
||||
|
||||
return {
|
||||
"destroy": RoleChoices.OWNER in roles,
|
||||
"destroy": role == RoleChoices.OWNER,
|
||||
"generate_document": can_get,
|
||||
"accesses_manage": is_owner_or_admin,
|
||||
"update": can_update,
|
||||
@@ -1352,11 +1374,65 @@ class TemplateAccess(BaseAccess):
|
||||
def __str__(self):
|
||||
return f"{self.user!s} is {self.role:s} in template {self.template!s}"
|
||||
|
||||
def get_role(self, user):
|
||||
"""
|
||||
Get the role a user has on a resource.
|
||||
"""
|
||||
if not user.is_authenticated:
|
||||
return None
|
||||
|
||||
try:
|
||||
roles = self.user_roles or []
|
||||
except AttributeError:
|
||||
teams = user.teams
|
||||
try:
|
||||
roles = self.template.accesses.filter(
|
||||
models.Q(user=user) | models.Q(team__in=teams),
|
||||
).values_list("role", flat=True)
|
||||
except (Template.DoesNotExist, IndexError):
|
||||
roles = []
|
||||
|
||||
return RoleChoices.max(*roles)
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""
|
||||
Compute and return abilities for a given user on the template access.
|
||||
"""
|
||||
return self._get_abilities(self.template, user)
|
||||
role = self.get_role(user)
|
||||
is_owner_or_admin = role in PRIVILEGED_ROLES
|
||||
|
||||
if self.role == RoleChoices.OWNER:
|
||||
can_delete = (role == RoleChoices.OWNER) and self.template.accesses.filter(
|
||||
role=RoleChoices.OWNER
|
||||
).count() > 1
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
)
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if role == RoleChoices.OWNER:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
set_role_to.remove(self.role)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to),
|
||||
"partial_update": bool(set_role_to),
|
||||
"retrieve": bool(role),
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
|
||||
|
||||
class Invitation(BaseModel):
|
||||
|
||||
@@ -9,7 +9,8 @@ from core import enums
|
||||
|
||||
AI_ACTIONS = {
|
||||
"prompt": (
|
||||
"Answer the prompt in markdown format. "
|
||||
"Answer the prompt using markdown formatting for structure and emphasis. "
|
||||
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
|
||||
"Preserve the language and markdown formatting. "
|
||||
"Do not provide any other information. "
|
||||
"Preserve the language."
|
||||
|
||||
@@ -41,3 +41,35 @@ class CollaborationService:
|
||||
f"Failed to notify WebSocket server. Status code: {response.status_code}, "
|
||||
f"Response: {response.text}"
|
||||
)
|
||||
|
||||
def get_document_connection_info(self, room, session_key):
|
||||
"""
|
||||
Get the connection info for a document.
|
||||
"""
|
||||
endpoint = "get-connections"
|
||||
querystring = {
|
||||
"room": room,
|
||||
"sessionKey": session_key,
|
||||
}
|
||||
endpoint_url = f"{settings.COLLABORATION_API_URL}{endpoint}/"
|
||||
|
||||
headers = {"Authorization": settings.COLLABORATION_SERVER_SECRET}
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
endpoint_url, headers=headers, params=querystring, timeout=10
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
raise requests.HTTPError("Failed to get document connection info.") from e
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result.get("count", 0), result.get("exists", False)
|
||||
|
||||
if response.status_code == 404:
|
||||
return 0, False
|
||||
|
||||
raise requests.HTTPError(
|
||||
f"Failed to get document connection info. Status code: {response.status_code}, "
|
||||
f"Response: {response.text}"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Converter services."""
|
||||
"""Y-Provider API services."""
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -17,14 +19,6 @@ class ServiceUnavailableError(ConversionError):
|
||||
"""Raised when the conversion service is unavailable."""
|
||||
|
||||
|
||||
class InvalidResponseError(ConversionError):
|
||||
"""Raised when the conversion service returns an invalid response."""
|
||||
|
||||
|
||||
class MissingContentError(ConversionError):
|
||||
"""Raised when the response is missing required content."""
|
||||
|
||||
|
||||
class YdocConverter:
|
||||
"""Service class for conversion-related operations."""
|
||||
|
||||
@@ -32,47 +26,47 @@ class YdocConverter:
|
||||
def auth_header(self):
|
||||
"""Build microservice authentication header."""
|
||||
# Note: Yprovider microservice accepts only raw token, which is not recommended
|
||||
return settings.Y_PROVIDER_API_KEY
|
||||
return f"Bearer {settings.Y_PROVIDER_API_KEY}"
|
||||
|
||||
def convert_markdown(self, text):
|
||||
def _request(self, url, data, content_type, accept):
|
||||
"""Make a request to the Y-Provider API."""
|
||||
response = requests.post(
|
||||
url,
|
||||
data=data,
|
||||
headers={
|
||||
"Authorization": self.auth_header,
|
||||
"Content-Type": content_type,
|
||||
"Accept": accept,
|
||||
},
|
||||
timeout=settings.CONVERSION_API_TIMEOUT,
|
||||
verify=settings.CONVERSION_API_SECURE,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def convert(
|
||||
self, text, content_type="text/markdown", accept="application/vnd.yjs.doc"
|
||||
):
|
||||
"""Convert a Markdown text into our internal format using an external microservice."""
|
||||
|
||||
if not text:
|
||||
raise ValidationError("Input text cannot be empty")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
response = self._request(
|
||||
f"{settings.Y_PROVIDER_API_BASE_URL}{settings.CONVERSION_API_ENDPOINT}/",
|
||||
json={
|
||||
"content": text,
|
||||
},
|
||||
headers={
|
||||
"Authorization": self.auth_header,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=settings.CONVERSION_API_TIMEOUT,
|
||||
verify=settings.CONVERSION_API_SECURE,
|
||||
text,
|
||||
content_type,
|
||||
accept,
|
||||
)
|
||||
response.raise_for_status()
|
||||
conversion_response = response.json()
|
||||
|
||||
if accept == "application/vnd.yjs.doc":
|
||||
return b64encode(response.content).decode("utf-8")
|
||||
if accept in {"text/markdown", "text/html"}:
|
||||
return response.text
|
||||
if accept == "application/json":
|
||||
return response.json()
|
||||
raise ValidationError("Unsupported format")
|
||||
except requests.RequestException as err:
|
||||
raise ServiceUnavailableError(
|
||||
"Failed to connect to conversion service",
|
||||
) from err
|
||||
|
||||
except ValueError as err:
|
||||
raise InvalidResponseError(
|
||||
"Could not parse conversion service response"
|
||||
) from err
|
||||
|
||||
try:
|
||||
document_content = conversion_response[
|
||||
settings.CONVERSION_API_CONTENT_FIELD
|
||||
]
|
||||
except KeyError as err:
|
||||
raise MissingContentError(
|
||||
f"Response missing required field: {settings.CONVERSION_API_CONTENT_FIELD}"
|
||||
) from err
|
||||
|
||||
return document_content
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Test document accesses API endpoints for users in impress's core app.
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import random
|
||||
from uuid import uuid4
|
||||
@@ -8,7 +9,7 @@ from uuid import uuid4
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core import choices, factories, models
|
||||
from core.api import serializers
|
||||
from core.tests.conftest import TEAM, USER, VIA
|
||||
from core.tests.test_services_collaboration_services import ( # pylint: disable=unused-import
|
||||
@@ -51,12 +52,7 @@ def test_api_document_accesses_list_authenticated_unrelated():
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
def test_api_document_accesses_list_unexisting_document():
|
||||
@@ -69,39 +65,46 @@ def test_api_document_accesses_list_unexisting_document():
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{uuid4()!s}/accesses/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize(
|
||||
"role", [role for role in models.RoleChoices if role not in models.PRIVILEGED_ROLES]
|
||||
"role",
|
||||
[role for role in choices.RoleChoices if role not in choices.PRIVILEGED_ROLES],
|
||||
)
|
||||
def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
via, role, mock_user_teams
|
||||
via, role, mock_user_teams, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be able to list document accesses for a document
|
||||
to which they are directly related, whatever their role in the document.
|
||||
Authenticated users with no privileged role should only be able to list document
|
||||
accesses associated with privileged roles for a document, including from ancestors.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
owner = factories.UserFactory()
|
||||
accesses = []
|
||||
|
||||
document_access = factories.UserDocumentAccessFactory(
|
||||
user=owner, role=models.RoleChoices.OWNER
|
||||
# Create documents structured as a tree
|
||||
unreadable_ancestor = factories.DocumentFactory(link_reach="restricted")
|
||||
# make all documents below the grand parent readable without a specific access for the user
|
||||
grand_parent = factories.DocumentFactory(
|
||||
parent=unreadable_ancestor, link_reach="authenticated"
|
||||
)
|
||||
accesses.append(document_access)
|
||||
document = document_access.document
|
||||
parent = factories.DocumentFactory(parent=grand_parent)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
|
||||
# Create accesses related to each document
|
||||
accesses = (
|
||||
factories.UserDocumentAccessFactory(document=unreadable_ancestor),
|
||||
factories.UserDocumentAccessFactory(document=grand_parent),
|
||||
factories.UserDocumentAccessFactory(document=parent),
|
||||
factories.UserDocumentAccessFactory(document=document),
|
||||
factories.TeamDocumentAccessFactory(document=document),
|
||||
)
|
||||
factories.UserDocumentAccessFactory(document=child)
|
||||
|
||||
if via == USER:
|
||||
models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
@@ -116,33 +119,32 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
role=role,
|
||||
)
|
||||
|
||||
access1 = factories.TeamDocumentAccessFactory(document=document)
|
||||
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||
accesses.append(access1)
|
||||
accesses.append(access2)
|
||||
|
||||
# Accesses for other documents to which the user is related should not be listed either
|
||||
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||
factories.UserDocumentAccessFactory(document=other_access.document)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
with django_assert_num_queries(3):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||
|
||||
# Return only owners
|
||||
owners_accesses = [
|
||||
access for access in accesses if access.role in models.PRIVILEGED_ROLES
|
||||
]
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert content["count"] == len(owners_accesses)
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
|
||||
# Make sure only privileged roles are returned
|
||||
privileged_accesses = [
|
||||
acc for acc in accesses if acc.role in choices.PRIVILEGED_ROLES
|
||||
]
|
||||
assert len(content) == len(privileged_accesses)
|
||||
|
||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access.id),
|
||||
"document": {
|
||||
"id": str(access.document_id),
|
||||
"path": access.document.path,
|
||||
"depth": access.document.depth,
|
||||
},
|
||||
"user": {
|
||||
"id": None,
|
||||
"email": None,
|
||||
"full_name": access.user.full_name,
|
||||
"short_name": access.user.short_name,
|
||||
}
|
||||
@@ -150,40 +152,47 @@ def test_api_document_accesses_list_authenticated_related_non_privileged(
|
||||
else None,
|
||||
"team": access.team,
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": access.role,
|
||||
"abilities": {
|
||||
"destroy": False,
|
||||
"partial_update": False,
|
||||
"retrieve": False,
|
||||
"set_role_to": [],
|
||||
"update": False,
|
||||
},
|
||||
}
|
||||
for access in owners_accesses
|
||||
for access in privileged_accesses
|
||||
],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
|
||||
for access in content["results"]:
|
||||
assert access["role"] in models.PRIVILEGED_ROLES
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("role", models.PRIVILEGED_ROLES)
|
||||
def test_api_document_accesses_list_authenticated_related_privileged_roles(
|
||||
via, role, mock_user_teams
|
||||
@pytest.mark.parametrize(
|
||||
"role", [role for role in choices.RoleChoices if role in choices.PRIVILEGED_ROLES]
|
||||
)
|
||||
def test_api_document_accesses_list_authenticated_related_privileged(
|
||||
via, role, mock_user_teams, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be able to list document accesses for a document
|
||||
to which they are directly related, whatever their role in the document.
|
||||
Authenticated users with a privileged role should be able to list all
|
||||
document accesses whatever the role, including from ancestors.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
owner = factories.UserFactory()
|
||||
accesses = []
|
||||
|
||||
document_access = factories.UserDocumentAccessFactory(
|
||||
user=owner, role=models.RoleChoices.OWNER
|
||||
# Create documents structured as a tree
|
||||
unreadable_ancestor = factories.DocumentFactory(link_reach="restricted")
|
||||
# make all documents below the grand parent readable without a specific access for the user
|
||||
grand_parent = factories.DocumentFactory(
|
||||
parent=unreadable_ancestor, link_reach="authenticated"
|
||||
)
|
||||
accesses.append(document_access)
|
||||
document = document_access.document
|
||||
user_access = None
|
||||
parent = factories.DocumentFactory(parent=grand_parent)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
|
||||
if via == USER:
|
||||
user_access = models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
@@ -197,61 +206,319 @@ def test_api_document_accesses_list_authenticated_related_privileged_roles(
|
||||
team="lasuite",
|
||||
role=role,
|
||||
)
|
||||
else:
|
||||
raise RuntimeError()
|
||||
|
||||
access1 = factories.TeamDocumentAccessFactory(document=document)
|
||||
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||
accesses.append(access1)
|
||||
accesses.append(access2)
|
||||
# Create accesses related to each document
|
||||
ancestors_accesses = [
|
||||
# Access on unreadable ancestor should still be listed
|
||||
# as the related user gains access to our document
|
||||
factories.UserDocumentAccessFactory(document=unreadable_ancestor),
|
||||
factories.UserDocumentAccessFactory(document=grand_parent),
|
||||
factories.UserDocumentAccessFactory(document=parent),
|
||||
]
|
||||
document_accesses = [
|
||||
factories.UserDocumentAccessFactory(document=document),
|
||||
factories.TeamDocumentAccessFactory(document=document),
|
||||
factories.UserDocumentAccessFactory(document=document),
|
||||
user_access,
|
||||
]
|
||||
factories.UserDocumentAccessFactory(document=child)
|
||||
|
||||
# Accesses for other documents to which the user is related should not be listed either
|
||||
other_access = factories.UserDocumentAccessFactory(user=user)
|
||||
factories.UserDocumentAccessFactory(document=other_access.document)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
|
||||
access2_user = serializers.UserSerializer(instance=access2.user).data
|
||||
base_user = serializers.UserSerializer(instance=user).data
|
||||
with django_assert_num_queries(3):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 4
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
assert len(content) == 7
|
||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(user_access.id),
|
||||
"user": base_user if via == "user" else None,
|
||||
"team": "lasuite" if via == "team" else "",
|
||||
"role": user_access.role,
|
||||
"abilities": user_access.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(access1.id),
|
||||
"user": None,
|
||||
"team": access1.team,
|
||||
"role": access1.role,
|
||||
"abilities": access1.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(access2.id),
|
||||
"user": access2_user,
|
||||
"team": "",
|
||||
"role": access2.role,
|
||||
"abilities": access2.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(document_access.id),
|
||||
"user": serializers.UserSerializer(instance=owner).data,
|
||||
"team": "",
|
||||
"role": models.RoleChoices.OWNER,
|
||||
"abilities": document_access.get_abilities(user),
|
||||
},
|
||||
"id": str(access.id),
|
||||
"document": {
|
||||
"id": str(access.document_id),
|
||||
"path": access.document.path,
|
||||
"depth": access.document.depth,
|
||||
},
|
||||
"user": {
|
||||
"id": str(access.user.id),
|
||||
"email": access.user.email,
|
||||
"language": access.user.language,
|
||||
"full_name": access.user.full_name,
|
||||
"short_name": access.user.short_name,
|
||||
}
|
||||
if access.user
|
||||
else None,
|
||||
"max_ancestors_role": None,
|
||||
"max_role": access.role,
|
||||
"team": access.team,
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
}
|
||||
for access in ancestors_accesses + document_accesses
|
||||
],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
|
||||
|
||||
def test_api_document_accesses_retrieve_set_role_to_child():
|
||||
"""Check set_role_to for an access with no access on the ancestor."""
|
||||
user, other_user = factories.UserFactory.create_batch(2)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory()
|
||||
parent_access = factories.UserDocumentAccessFactory(
|
||||
document=parent, user=user, role="owner"
|
||||
)
|
||||
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
document_access_other_user = factories.UserDocumentAccessFactory(
|
||||
document=document, user=other_user, role="editor"
|
||||
)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content) == 2
|
||||
|
||||
result_dict = {
|
||||
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||
}
|
||||
assert result_dict[str(document_access_other_user.id)] == [
|
||||
"reader",
|
||||
"editor",
|
||||
"administrator",
|
||||
"owner",
|
||||
]
|
||||
assert result_dict[str(parent_access.id)] == []
|
||||
|
||||
# Add an access for the other user on the parent
|
||||
parent_access_other_user = factories.UserDocumentAccessFactory(
|
||||
document=parent, user=other_user, role="editor"
|
||||
)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content) == 3
|
||||
|
||||
result_dict = {
|
||||
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||
}
|
||||
assert result_dict[str(document_access_other_user.id)] == [
|
||||
"editor",
|
||||
"administrator",
|
||||
"owner",
|
||||
]
|
||||
assert result_dict[str(parent_access.id)] == []
|
||||
assert result_dict[str(parent_access_other_user.id)] == [
|
||||
"reader",
|
||||
"editor",
|
||||
"administrator",
|
||||
"owner",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"roles,results",
|
||||
[
|
||||
[
|
||||
["administrator", "reader", "reader", "reader"],
|
||||
[
|
||||
["reader", "editor", "administrator"],
|
||||
[],
|
||||
[],
|
||||
["reader", "editor", "administrator"],
|
||||
],
|
||||
],
|
||||
[
|
||||
["owner", "reader", "reader", "reader"],
|
||||
[
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
[],
|
||||
[],
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
],
|
||||
],
|
||||
[
|
||||
["owner", "reader", "reader", "owner"],
|
||||
[
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
[],
|
||||
[],
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
],
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_api_document_accesses_list_authenticated_related_same_user(roles, results):
|
||||
"""
|
||||
The maximum role across ancestor documents and set_role_to optionsfor
|
||||
a given user should be filled as expected.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create documents structured as a tree
|
||||
grand_parent = factories.DocumentFactory(link_reach="authenticated")
|
||||
parent = factories.DocumentFactory(parent=grand_parent)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
|
||||
# Create accesses for another user
|
||||
other_user = factories.UserFactory()
|
||||
accesses = [
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role=roles[0]
|
||||
),
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=grand_parent, user=other_user, role=roles[1]
|
||||
),
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=parent, user=other_user, role=roles[2]
|
||||
),
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=other_user, role=roles[3]
|
||||
),
|
||||
]
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content) == 4
|
||||
|
||||
for result in content:
|
||||
assert (
|
||||
result["max_ancestors_role"] is None
|
||||
if result["user"]["id"] == str(user.id)
|
||||
else choices.RoleChoices.max(roles[1], roles[2])
|
||||
)
|
||||
|
||||
result_dict = {
|
||||
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||
}
|
||||
assert [result_dict[str(access.id)] for access in accesses] == results
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"roles,results",
|
||||
[
|
||||
[
|
||||
["administrator", "reader", "reader", "reader"],
|
||||
[
|
||||
["reader", "editor", "administrator"],
|
||||
[],
|
||||
[],
|
||||
["reader", "editor", "administrator"],
|
||||
],
|
||||
],
|
||||
[
|
||||
["owner", "reader", "reader", "reader"],
|
||||
[
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
[],
|
||||
[],
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
],
|
||||
],
|
||||
[
|
||||
["owner", "reader", "reader", "owner"],
|
||||
[
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
[],
|
||||
[],
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
],
|
||||
],
|
||||
[
|
||||
["reader", "reader", "reader", "owner"],
|
||||
[
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
[],
|
||||
[],
|
||||
["reader", "editor", "administrator", "owner"],
|
||||
],
|
||||
],
|
||||
[
|
||||
["reader", "administrator", "reader", "editor"],
|
||||
[
|
||||
["reader", "editor", "administrator"],
|
||||
["reader", "editor", "administrator"],
|
||||
[],
|
||||
[],
|
||||
],
|
||||
],
|
||||
[
|
||||
["editor", "editor", "administrator", "editor"],
|
||||
[
|
||||
["reader", "editor", "administrator"],
|
||||
[],
|
||||
["editor", "administrator"],
|
||||
[],
|
||||
],
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_api_document_accesses_list_authenticated_related_same_team(
|
||||
roles, results, mock_user_teams
|
||||
):
|
||||
"""
|
||||
The maximum role across ancestor documents and set_role_to optionsfor
|
||||
a given team should be filled as expected.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Create documents structured as a tree
|
||||
grand_parent = factories.DocumentFactory(link_reach="authenticated")
|
||||
parent = factories.DocumentFactory(parent=grand_parent)
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
accesses = [
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role=roles[0]
|
||||
),
|
||||
# Create accesses for a team
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=grand_parent, team="lasuite", role=roles[1]
|
||||
),
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=parent, team="lasuite", role=roles[2]
|
||||
),
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role=roles[3]
|
||||
),
|
||||
]
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content) == 4
|
||||
|
||||
for result in content:
|
||||
assert (
|
||||
result["max_ancestors_role"] is None
|
||||
if result["user"] and result["user"]["id"] == str(user.id)
|
||||
else choices.RoleChoices.max(roles[1], roles[2])
|
||||
)
|
||||
|
||||
result_dict = {
|
||||
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||
}
|
||||
assert [result_dict[str(access.id)] for access in accesses] == results
|
||||
|
||||
|
||||
def test_api_document_accesses_retrieve_anonymous():
|
||||
"""
|
||||
Anonymous users should not be allowed to retrieve a document access.
|
||||
@@ -307,7 +574,9 @@ def test_api_document_accesses_retrieve_authenticated_unrelated():
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("role", models.RoleChoices)
|
||||
def test_api_document_accesses_retrieve_authenticated_related(
|
||||
via, role, mock_user_teams
|
||||
via,
|
||||
role,
|
||||
mock_user_teams,
|
||||
):
|
||||
"""
|
||||
A user who is related to a document should be allowed to retrieve the
|
||||
@@ -333,7 +602,7 @@ def test_api_document_accesses_retrieve_authenticated_related(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
if not role in models.PRIVILEGED_ROLES:
|
||||
if not role in choices.PRIVILEGED_ROLES:
|
||||
assert response.status_code == 403
|
||||
else:
|
||||
access_user = serializers.UserSerializer(instance=access.user).data
|
||||
@@ -341,9 +610,16 @@ def test_api_document_accesses_retrieve_authenticated_related(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(access.id),
|
||||
"document": {
|
||||
"id": str(access.document_id),
|
||||
"path": access.document.path,
|
||||
"depth": access.document.depth,
|
||||
},
|
||||
"user": access_user,
|
||||
"team": "",
|
||||
"role": access.role,
|
||||
"max_ancestors_role": None,
|
||||
"max_role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
}
|
||||
|
||||
@@ -448,7 +724,9 @@ def test_api_document_accesses_update_authenticated_reader_or_editor(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("create_for", VIA)
|
||||
def test_api_document_accesses_update_administrator_except_owner(
|
||||
create_for,
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
@@ -481,32 +759,31 @@ def test_api_document_accesses_update_administrator_except_owner(
|
||||
|
||||
new_values = {
|
||||
"id": uuid4(),
|
||||
"user_id": factories.UserFactory().id,
|
||||
"role": random.choice(["administrator", "editor", "reader"]),
|
||||
}
|
||||
if create_for == USER:
|
||||
new_values["user_id"] = factories.UserFactory().id
|
||||
elif create_for == TEAM:
|
||||
new_values["team"] = "new-team"
|
||||
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
if new_data["role"] == old_values["role"]:
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
else:
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 200
|
||||
|
||||
access.refresh_from_db()
|
||||
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
if field == "role":
|
||||
assert updated_values == {**old_values, "role": new_values["role"]}
|
||||
if field in ["role", "max_role"]:
|
||||
assert updated_values == {
|
||||
**old_values,
|
||||
"role": new_values["role"],
|
||||
"max_role": new_values["role"],
|
||||
}
|
||||
else:
|
||||
assert updated_values == old_values
|
||||
|
||||
@@ -601,7 +878,7 @@ def test_api_document_accesses_update_administrator_to_owner(
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
# We are not allowed or not really updating the role
|
||||
if field == "role" or new_data["role"] == old_values["role"]:
|
||||
if field == "role":
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
@@ -624,7 +901,9 @@ def test_api_document_accesses_update_administrator_to_owner(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize("create_for", VIA)
|
||||
def test_api_document_accesses_update_owner(
|
||||
create_for,
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
@@ -655,42 +934,39 @@ def test_api_document_accesses_update_owner(
|
||||
|
||||
new_values = {
|
||||
"id": uuid4(),
|
||||
"user_id": factories.UserFactory().id,
|
||||
"role": random.choice(models.RoleChoices.values),
|
||||
}
|
||||
if create_for == USER:
|
||||
new_values["user_id"] = factories.UserFactory().id
|
||||
elif create_for == TEAM:
|
||||
new_values["team"] = "new-team"
|
||||
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
if (
|
||||
new_data["role"] == old_values["role"]
|
||||
): # we are not really updating the role
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
else:
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data=new_data,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 200
|
||||
|
||||
access.refresh_from_db()
|
||||
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
|
||||
if field == "role":
|
||||
assert updated_values == {**old_values, "role": new_values["role"]}
|
||||
if field in ["role", "max_role"]:
|
||||
assert updated_values == {
|
||||
**old_values,
|
||||
"role": new_values["role"],
|
||||
"max_role": new_values["role"],
|
||||
}
|
||||
else:
|
||||
assert updated_values == old_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_update_owner_self(
|
||||
def test_api_document_accesses_update_owner_self_root(
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
@@ -751,6 +1027,51 @@ def test_api_document_accesses_update_owner_self(
|
||||
assert access.role == new_role
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_update_owner_self_child(
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
):
|
||||
"""
|
||||
A user who is owner of a document should be allowed to update
|
||||
their own user access even if they are the only owner in the document,
|
||||
provided the document is not a root.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory()
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
access = None
|
||||
if via == USER:
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
access = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
new_role = random.choice(["administrator", "editor", "reader"])
|
||||
|
||||
user_id = str(access.user_id) if via == USER else None
|
||||
with mock_reset_connections(document.id, user_id):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data={**old_values, "role": new_role},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
access.refresh_from_db()
|
||||
assert access.role == new_role
|
||||
|
||||
|
||||
# Delete
|
||||
|
||||
|
||||
@@ -931,17 +1252,16 @@ def test_api_document_accesses_delete_owners(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
assert models.DocumentAccess.objects.count() == 1
|
||||
assert response.status_code == 204
|
||||
assert models.DocumentAccess.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_delete_owners_last_owner(via, mock_user_teams):
|
||||
def test_api_document_accesses_delete_owners_last_owner_root(via, mock_user_teams):
|
||||
"""
|
||||
It should not be possible to delete the last owner access from a document
|
||||
It should not be possible to delete the last owner access from a root document
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
@@ -964,3 +1284,63 @@ def test_api_document_accesses_delete_owners_last_owner(via, mock_user_teams):
|
||||
|
||||
assert response.status_code == 403
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
|
||||
|
||||
def test_api_document_accesses_delete_owners_last_owner_child_user(
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
):
|
||||
"""
|
||||
It should be possible to delete the last owner access from a document that is not a root.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory()
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
access = None
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="owner"
|
||||
)
|
||||
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
assert models.DocumentAccess.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="Pending fix on https://github.com/suitenumerique/docs/issues/969"
|
||||
)
|
||||
def test_api_document_accesses_delete_owners_last_owner_child_team(
|
||||
mock_user_teams,
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
):
|
||||
"""
|
||||
It should be possible to delete the last owner access from a document that
|
||||
is not a root.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory()
|
||||
document = factories.DocumentFactory(parent=parent)
|
||||
access = None
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
access = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
assert models.DocumentAccess.objects.count() == 1
|
||||
|
||||
@@ -103,32 +103,37 @@ def test_api_document_accesses_create_authenticated_reader_or_editor(
|
||||
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_administrator(via, mock_user_teams):
|
||||
def test_api_document_accesses_create_authenticated_administrator_share_to_user(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Administrators of a document should be able to create document accesses
|
||||
except for the "owner" role.
|
||||
Administrators of a document (direct or by heritage) should be able to create
|
||||
document accesses except for the "owner" role.
|
||||
An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="administrator"
|
||||
document=documents[0], user=user, role="administrator"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="administrator"
|
||||
document=documents[0], team="lasuite", role="administrator"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
|
||||
# It should not be allowed to create an owner access
|
||||
document = documents[-1]
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
@@ -140,7 +145,7 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Only owners of a resource can assign other users as owners."
|
||||
"detail": "Only owners of a document can assign other users as owners."
|
||||
}
|
||||
|
||||
# It should be allowed to create a lower access
|
||||
@@ -165,9 +170,16 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"depth": new_document_access.document.depth,
|
||||
"path": new_document_access.document.path,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"team": "",
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "",
|
||||
"user": other_user,
|
||||
}
|
||||
assert len(mail.outbox) == 1
|
||||
@@ -182,28 +194,119 @@ def test_api_document_accesses_create_authenticated_administrator(via, mock_user
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
def test_api_document_accesses_create_authenticated_administrator_share_to_team(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Owners of a document should be able to create document accesses whatever the role.
|
||||
Administrators of a document (direct or by heritage) should be able to create
|
||||
document accesses except for the "owner" role.
|
||||
An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=documents[0], user=user, role="administrator"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=documents[0], team="lasuite", role="administrator"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
document = documents[-1]
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"team": "new-team",
|
||||
"role": "owner",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Only owners of a document can assign other users as owners."
|
||||
}
|
||||
|
||||
# It should be allowed to create a lower access
|
||||
role = random.choice(
|
||||
[role[0] for role in models.RoleChoices.choices if role[0] != "owner"]
|
||||
)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"team": "new-team",
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(team="new-team").count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(team="new-team").get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"depth": new_document_access.document.depth,
|
||||
"path": new_document_access.document.path,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "new-team",
|
||||
"user": None,
|
||||
}
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner_share_to_user(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Owners of a document (direct or by heritage) should be able to create document accesses
|
||||
whatever the role. An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=documents[0], user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="owner"
|
||||
document=documents[0], team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
|
||||
document = documents[-1]
|
||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
@@ -222,11 +325,18 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user,
|
||||
"team": "",
|
||||
"role": role,
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"depth": new_document_access.document.depth,
|
||||
"path": new_document_access.document.path,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "",
|
||||
"user": other_user,
|
||||
}
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
@@ -240,6 +350,71 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_teams):
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner_share_to_team(
|
||||
via, depth, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Owners of a document (direct or by heritage) should be able to create document accesses
|
||||
whatever the role. An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents = []
|
||||
for i in range(depth):
|
||||
parent = documents[i - 1] if i > 0 else None
|
||||
documents.append(factories.DocumentFactory(parent=parent))
|
||||
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=documents[0], user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=documents[0], team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory(language="en-us")
|
||||
document = documents[-1]
|
||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"team": "new-team",
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(team="new-team").count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(team="new-team").get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"path": new_document_access.document.path,
|
||||
"depth": new_document_access.document.depth,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "new-team",
|
||||
"user": None,
|
||||
}
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_email_in_receivers_language(via, mock_user_teams):
|
||||
"""
|
||||
@@ -286,11 +461,18 @@ def test_api_document_accesses_create_email_in_receivers_language(via, mock_user
|
||||
).get()
|
||||
other_user_data = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user_data,
|
||||
"team": "",
|
||||
"role": role,
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"document": {
|
||||
"id": str(new_document_access.document_id),
|
||||
"path": new_document_access.document.path,
|
||||
"depth": new_document_access.document.depth,
|
||||
},
|
||||
"id": str(new_document_access.id),
|
||||
"max_ancestors_role": None,
|
||||
"max_role": role,
|
||||
"role": role,
|
||||
"team": "",
|
||||
"user": other_user_data,
|
||||
}
|
||||
assert len(mail.outbox) == index + 1
|
||||
email = mail.outbox[index]
|
||||
|
||||
@@ -175,8 +175,11 @@ def test_api_documents_ai_transform_authenticated_success(mock_create, reach, ro
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Answer the prompt in markdown format. Preserve the language and markdown "
|
||||
"formatting. Do not provide any other information. Preserve the language."
|
||||
"Answer the prompt using markdown formatting for structure and emphasis. "
|
||||
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
|
||||
"Preserve the language and markdown formatting. "
|
||||
"Do not provide any other information. "
|
||||
"Preserve the language."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": "Hello"},
|
||||
@@ -249,8 +252,11 @@ def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_te
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Answer the prompt in markdown format. Preserve the language and markdown "
|
||||
"formatting. Do not provide any other information. Preserve the language."
|
||||
"Answer the prompt using markdown formatting for structure and emphasis. "
|
||||
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
|
||||
"Preserve the language and markdown formatting. "
|
||||
"Do not provide any other information. "
|
||||
"Preserve the language."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": "Hello"},
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_api_documents_ask_for_access_create_authenticated():
|
||||
An email should be sent to document owners and admins to notify them.
|
||||
"""
|
||||
owner_user = UserFactory(language="en-us")
|
||||
admin_user = UserFactory(language="fr-fr")
|
||||
admin_user = UserFactory(language="en-us")
|
||||
document = DocumentFactory(
|
||||
users=[
|
||||
(owner_user, RoleChoices.OWNER),
|
||||
@@ -97,6 +97,23 @@ def test_api_documents_ask_for_access_create_authenticated():
|
||||
assert document.title.lower() in email_subject.lower()
|
||||
|
||||
|
||||
def test_api_documents_ask_for_access_create_authenticated_non_root_document():
|
||||
"""
|
||||
Authenticated users should not be able to create a document ask for access on a non-root
|
||||
document.
|
||||
"""
|
||||
parent = DocumentFactory()
|
||||
child = DocumentFactory(parent=parent)
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(f"/api/v1.0/documents/{child.id}/ask-for-access/")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_api_documents_ask_for_access_create_authenticated_specific_role():
|
||||
"""
|
||||
Authenticated users should be able to create a document ask for access with a specific role.
|
||||
@@ -196,6 +213,20 @@ def test_api_documents_ask_for_access_list_authenticated():
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_ask_for_access_list_authenticated_non_root_document():
|
||||
"""
|
||||
Authenticated users should not be able to list document ask for access on a non-root document.
|
||||
"""
|
||||
parent = DocumentFactory()
|
||||
child = DocumentFactory(parent=parent)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(UserFactory())
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{child.id}/ask-for-access/")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_api_documents_ask_for_access_list_authenticated_own_request():
|
||||
"""Authenticated users should be able to list their own document ask for access."""
|
||||
document = DocumentFactory()
|
||||
@@ -289,7 +320,7 @@ def test_api_documents_ask_for_access_list_non_owner_or_admin(role):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", [RoleChoices.OWNER])
|
||||
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
|
||||
def test_api_documents_ask_for_access_list_owner_or_admin(role):
|
||||
"""Owner or admin users should be able to list document ask for access."""
|
||||
user = UserFactory()
|
||||
@@ -329,6 +360,23 @@ def test_api_documents_ask_for_access_list_owner_or_admin(role):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
|
||||
def test_api_documents_ask_for_access_list_admin_non_root_document(role):
|
||||
"""
|
||||
Authenticated users should not be able to list document ask for access on a non-root document.
|
||||
"""
|
||||
user = UserFactory()
|
||||
parent = DocumentFactory(users=[(user, role)])
|
||||
child = DocumentFactory(parent=parent, users=[(user, role)])
|
||||
DocumentAskForAccessFactory.create_batch(3, document=child, role=RoleChoices.READER)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{child.id}/ask-for-access/")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
## Retrieve
|
||||
|
||||
|
||||
@@ -415,6 +463,28 @@ def test_api_documents_ask_for_access_retrieve_owner_or_admin(role):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
|
||||
def test_api_documents_ask_for_access_retrieve_authenticated_non_root_document(role):
|
||||
"""
|
||||
Authenticated users should not be able to retrieve document ask for access on a non-root
|
||||
document.
|
||||
"""
|
||||
user = UserFactory()
|
||||
parent = DocumentFactory(users=[(user, role)])
|
||||
child = DocumentFactory(parent=parent, users=[(user, role)])
|
||||
document_ask_for_access = DocumentAskForAccessFactory(
|
||||
document=child, role=RoleChoices.READER
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{child.id}/ask-for-access/{document_ask_for_access.id}/"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
## Delete
|
||||
|
||||
|
||||
@@ -487,6 +557,28 @@ def test_api_documents_ask_for_access_delete_owner_or_admin(role):
|
||||
).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
|
||||
def test_api_documents_ask_for_access_delete_authenticated_non_root_document(role):
|
||||
"""
|
||||
Authenticated users should not be able to delete document ask for access on a non-root
|
||||
document.
|
||||
"""
|
||||
user = UserFactory()
|
||||
parent = DocumentFactory(users=[(user, role)])
|
||||
child = DocumentFactory(parent=parent, users=[(user, role)])
|
||||
document_ask_for_access = DocumentAskForAccessFactory(
|
||||
document=child, role=RoleChoices.READER
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{child.id}/ask-for-access/{document_ask_for_access.id}/"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
## Accept
|
||||
|
||||
|
||||
@@ -654,3 +746,25 @@ def test_api_documents_ask_for_access_accept_authenticated_owner_or_admin_update
|
||||
).exists()
|
||||
document_access.refresh_from_db()
|
||||
assert document_access.role == RoleChoices.ADMIN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
|
||||
def test_api_documents_ask_for_access_accept_authenticated_non_root_document(role):
|
||||
"""
|
||||
Authenticated users should not be able to accept document ask for access on a non-root
|
||||
document.
|
||||
"""
|
||||
user = UserFactory()
|
||||
parent = DocumentFactory(users=[(user, role)])
|
||||
child = DocumentFactory(parent=parent, users=[(user, role)])
|
||||
document_ask_for_access = DocumentAskForAccessFactory(
|
||||
document=child, role=RoleChoices.READER
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{child.id}/ask-for-access/{document_ask_for_access.id}/accept/"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
"""Test the can_edit endpoint in the viewset DocumentViewSet."""
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.parametrize("ws_not_connected_ready_only", [True, False])
|
||||
@pytest.mark.parametrize("role", ["editor", "reader"])
|
||||
def test_api_documents_can_edit_anonymous(settings, ws_not_connected_ready_only, role):
|
||||
"""Anonymous users can edit documents when link_role is editor."""
|
||||
document = factories.DocumentFactory(link_reach="public", link_role=role)
|
||||
client = APIClient()
|
||||
session_key = client.session.session_key
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = ws_not_connected_ready_only
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/can-edit/")
|
||||
|
||||
if role == "reader":
|
||||
assert response.status_code == 401
|
||||
else:
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
assert ws_resp.call_count == (1 if ws_not_connected_ready_only else 0)
|
||||
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.parametrize("ws_not_connected_ready_only", [True, False])
|
||||
def test_api_documents_can_edit_authenticated_no_websocket(
|
||||
settings, ws_not_connected_ready_only
|
||||
):
|
||||
"""
|
||||
A user not connected to the websocket and no other user have already updated the document,
|
||||
the document can be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = ws_not_connected_ready_only
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert response.json() == {"can_edit": True}
|
||||
assert ws_resp.call_count == (1 if ws_not_connected_ready_only else 0)
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_can_edit_authenticated_no_websocket_user_already_editing(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
A user not connected to the websocket and another user have already updated the document,
|
||||
the document can not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": False}
|
||||
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_can_edit_no_websocket_other_user_connected_to_websocket(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
A user not connected to the websocket and another user is connected to the websocket,
|
||||
the document can not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": False})
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": False}
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_can_edit_user_connected_to_websocket(settings):
|
||||
"""
|
||||
A user connected to the websocket, the document can be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": True})
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_websocket(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the document can be updated like if the user was
|
||||
not connected to the websocket.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=500)
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the behavior fallback to the no websocket one.
|
||||
If an other user is already editing, the document can not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=500)
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": False}
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_can_edit_websocket_server_room_not_found(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server returns a 404, the document can be updated like if the user was
|
||||
not connected to the websocket.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=404)
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_can_edit_websocket_server_room_not_found_other_already_editing(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server returns a 404 and another user is editing the document,
|
||||
the response should be can-edit=False.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=404)
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": False}
|
||||
|
||||
assert ws_resp.call_count == 1
|
||||
@@ -98,7 +98,9 @@ def test_api_documents_children_create_authenticated_success(reach, role, depth)
|
||||
if i == 0:
|
||||
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||
else:
|
||||
document = factories.DocumentFactory(parent=document, link_role="reader")
|
||||
document = factories.DocumentFactory(
|
||||
parent=document, link_reach="restricted"
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
@@ -112,7 +114,8 @@ def test_api_documents_children_create_authenticated_success(reach, role, depth)
|
||||
child = Document.objects.get(id=response.json()["id"])
|
||||
assert child.title == "my child"
|
||||
assert child.link_reach == "restricted"
|
||||
assert child.accesses.filter(role="owner", user=user).exists()
|
||||
# Access objects on the child are not necessary
|
||||
assert child.accesses.exists() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("depth", [1, 2, 3])
|
||||
@@ -180,7 +183,8 @@ def test_api_documents_children_create_related_success(role, depth):
|
||||
child = Document.objects.get(id=response.json()["id"])
|
||||
assert child.title == "my child"
|
||||
assert child.link_reach == "restricted"
|
||||
assert child.accesses.filter(role="owner", user=user).exists()
|
||||
# Access objects on the child are not necessary
|
||||
assert child.accesses.exists() is False
|
||||
|
||||
|
||||
def test_api_documents_children_create_authenticated_title_null():
|
||||
|
||||
@@ -14,13 +14,18 @@ from core import factories
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_documents_children_list_anonymous_public_standalone():
|
||||
def test_api_documents_children_list_anonymous_public_standalone(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Anonymous users should be allowed to retrieve the children of a public document."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(8):
|
||||
APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(4):
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -30,6 +35,10 @@ def test_api_documents_children_list_anonymous_public_standalone():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -44,10 +53,14 @@ def test_api_documents_children_list_anonymous_public_standalone():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -62,13 +75,13 @@ def test_api_documents_children_list_anonymous_public_standalone():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_anonymous_public_parent():
|
||||
def test_api_documents_children_list_anonymous_public_parent(django_assert_num_queries):
|
||||
"""
|
||||
Anonymous users should be allowed to retrieve the children of a document who
|
||||
has a public ancestor.
|
||||
@@ -83,7 +96,10 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(9):
|
||||
APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(5):
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -93,6 +109,10 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -107,10 +127,14 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -125,7 +149,7 @@ def test_api_documents_children_list_anonymous_public_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -149,7 +173,7 @@ def test_api_documents_children_list_anonymous_restricted_or_authenticated(reach
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_children_list_authenticated_unrelated_public_or_authenticated(
|
||||
reach,
|
||||
reach, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be able to retrieve the children of a public/authenticated
|
||||
@@ -163,9 +187,13 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(9):
|
||||
client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(5):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 2,
|
||||
@@ -174,6 +202,10 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -188,10 +220,14 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -206,7 +242,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -214,7 +250,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_children_list_authenticated_public_or_authenticated_parent(
|
||||
reach,
|
||||
reach, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document who
|
||||
@@ -231,7 +267,11 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(10):
|
||||
client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
with django_assert_num_queries(6):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
@@ -241,6 +281,10 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -255,10 +299,14 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -273,13 +321,15 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_unrelated_restricted():
|
||||
def test_api_documents_children_list_authenticated_unrelated_restricted(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should not be allowed to retrieve the children of a document that is
|
||||
restricted and to which they are not related.
|
||||
@@ -293,16 +343,20 @@ def test_api_documents_children_list_authenticated_unrelated_restricted():
|
||||
child1, _child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(2):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_direct():
|
||||
def test_api_documents_children_list_authenticated_related_direct(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document
|
||||
to which they are directly related whatever the role.
|
||||
@@ -319,10 +373,13 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(9):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
link_role = None if document.link_reach == "restricted" else document.link_role
|
||||
assert response.json() == {
|
||||
"count": 2,
|
||||
"next": None,
|
||||
@@ -330,6 +387,10 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": document.link_reach,
|
||||
"ancestors_link_role": link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -344,10 +405,14 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": document.link_reach,
|
||||
"ancestors_link_role": link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -362,13 +427,15 @@ def test_api_documents_children_list_authenticated_related_direct():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_parent():
|
||||
def test_api_documents_children_list_authenticated_related_parent(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document if they
|
||||
are related to one of its ancestors whatever the role.
|
||||
@@ -389,9 +456,11 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
document=grand_parent, user=user
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(10):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 2,
|
||||
@@ -400,6 +469,10 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -414,10 +487,14 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -432,13 +509,15 @@ def test_api_documents_children_list_authenticated_related_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_child():
|
||||
def test_api_documents_children_list_authenticated_related_child(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Authenticated users should not be allowed to retrieve all the children of a document
|
||||
as a result of being related to one of its children.
|
||||
@@ -454,16 +533,20 @@ def test_api_documents_children_list_authenticated_related_child():
|
||||
factories.UserDocumentAccessFactory(document=child1, user=user)
|
||||
factories.UserDocumentAccessFactory(document=document)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
with django_assert_num_queries(2):
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/children/",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_team_none(mock_user_teams):
|
||||
def test_api_documents_children_list_authenticated_related_team_none(
|
||||
mock_user_teams, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should not be able to retrieve the children of a restricted document
|
||||
related to teams in which the user is not.
|
||||
@@ -480,7 +563,9 @@ def test_api_documents_children_list_authenticated_related_team_none(mock_user_t
|
||||
|
||||
factories.TeamDocumentAccessFactory(document=document, team="myteam")
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(2):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
@@ -488,7 +573,7 @@ def test_api_documents_children_list_authenticated_related_team_none(mock_user_t
|
||||
|
||||
|
||||
def test_api_documents_children_list_authenticated_related_team_members(
|
||||
mock_user_teams,
|
||||
mock_user_teams, django_assert_num_queries
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve the children of a document to which they
|
||||
@@ -506,7 +591,8 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
|
||||
access = factories.TeamDocumentAccessFactory(document=document, team="myteam")
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
with django_assert_num_queries(9):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/children/")
|
||||
|
||||
# pylint: disable=R0801
|
||||
assert response.status_code == 200
|
||||
@@ -517,6 +603,10 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -531,10 +621,14 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -549,7 +643,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: content
|
||||
"""
|
||||
|
||||
import base64
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, role",
|
||||
[
|
||||
("public", "reader"),
|
||||
("public", "editor"),
|
||||
],
|
||||
)
|
||||
@patch("core.services.converter_services.YdocConverter.convert")
|
||||
def test_api_documents_content_public(mock_content, reach, role):
|
||||
"""Anonymous users should be allowed to access content of public documents."""
|
||||
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||
mock_content.return_value = {"some": "data"}
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == str(document.id)
|
||||
assert data["title"] == document.title
|
||||
assert data["content"] == {"some": "data"}
|
||||
mock_content.assert_called_once_with(
|
||||
base64.b64decode(document.content),
|
||||
"application/vnd.yjs.doc",
|
||||
"application/json",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, doc_role, user_role",
|
||||
[
|
||||
("restricted", "reader", "reader"),
|
||||
("restricted", "reader", "editor"),
|
||||
("restricted", "reader", "administrator"),
|
||||
("restricted", "reader", "owner"),
|
||||
("restricted", "editor", "reader"),
|
||||
("restricted", "editor", "editor"),
|
||||
("restricted", "editor", "administrator"),
|
||||
("restricted", "editor", "owner"),
|
||||
("authenticated", "reader", None),
|
||||
("authenticated", "editor", None),
|
||||
],
|
||||
)
|
||||
@patch("core.services.converter_services.YdocConverter.convert")
|
||||
def test_api_documents_content_not_public(mock_content, reach, doc_role, user_role):
|
||||
"""Authenticated users need access to get non-public document content."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory(link_reach=reach, link_role=doc_role)
|
||||
mock_content.return_value = {"some": "data"}
|
||||
|
||||
# First anonymous request should fail
|
||||
client = APIClient()
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
mock_content.assert_not_called()
|
||||
|
||||
# Login and try again
|
||||
client.force_login(user)
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
|
||||
|
||||
# If restricted, we still should not have access
|
||||
if user_role is not None:
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
mock_content.assert_not_called()
|
||||
|
||||
# Create an access as a reader. This should unlock the access.
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role=user_role
|
||||
)
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/content/")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == str(document.id)
|
||||
assert data["title"] == document.title
|
||||
assert data["content"] == {"some": "data"}
|
||||
mock_content.assert_called_once_with(
|
||||
base64.b64decode(document.content),
|
||||
"application/vnd.yjs.doc",
|
||||
"application/json",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"content_format, accept",
|
||||
[
|
||||
("markdown", "text/markdown"),
|
||||
("html", "text/html"),
|
||||
("json", "application/json"),
|
||||
],
|
||||
)
|
||||
@patch("core.services.converter_services.YdocConverter.convert")
|
||||
def test_api_documents_content_format(mock_content, content_format, accept):
|
||||
"""Test that the content endpoint returns a specific format."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
mock_content.return_value = {"some": "data"}
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/?content_format={content_format}"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == str(document.id)
|
||||
assert data["title"] == document.title
|
||||
assert data["content"] == {"some": "data"}
|
||||
mock_content.assert_called_once_with(
|
||||
base64.b64decode(document.content), "application/vnd.yjs.doc", accept
|
||||
)
|
||||
|
||||
|
||||
@patch("core.services.converter_services.YdocConverter._request")
|
||||
def test_api_documents_content_invalid_format(mock_request):
|
||||
"""Test that the content endpoint rejects invalid formats."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/?content_format=invalid"
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
mock_request.assert_not_called()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.YdocConverter._request")
|
||||
def test_api_documents_content_yservice_error(mock_request):
|
||||
"""Test that service errors are handled properly."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
mock_request.side_effect = requests.RequestException()
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
|
||||
mock_request.assert_called_once()
|
||||
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
|
||||
|
||||
@patch("core.services.converter_services.YdocConverter._request")
|
||||
def test_api_documents_content_nonexistent_document(mock_request):
|
||||
"""Test that accessing a nonexistent document returns 404."""
|
||||
client = APIClient()
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/00000000-0000-0000-0000-000000000000/content/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
mock_request.assert_not_called()
|
||||
|
||||
|
||||
@patch("core.services.converter_services.YdocConverter._request")
|
||||
def test_api_documents_content_empty_document(mock_request):
|
||||
"""Test that accessing an empty document returns empty content."""
|
||||
document = factories.DocumentFactory(link_reach="public", content="")
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/content/")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == str(document.id)
|
||||
assert data["title"] == document.title
|
||||
assert data["content"] is None
|
||||
mock_request.assert_not_called()
|
||||
@@ -23,10 +23,10 @@ pytestmark = pytest.mark.django_db
|
||||
|
||||
@pytest.fixture
|
||||
def mock_convert_md():
|
||||
"""Mock YdocConverter.convert_markdown to return a converted content."""
|
||||
"""Mock YdocConverter.convert to return a converted content."""
|
||||
with patch.object(
|
||||
YdocConverter,
|
||||
"convert_markdown",
|
||||
"convert",
|
||||
return_value="Converted document content",
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
@@ -32,6 +32,10 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -46,10 +50,16 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": "editor"
|
||||
if (child1.link_reach == "public" and child1.link_role == "editor")
|
||||
else document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"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),
|
||||
"depth": 3,
|
||||
@@ -64,10 +74,14 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -82,7 +96,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -115,6 +129,10 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -129,10 +147,14 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": "public",
|
||||
"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),
|
||||
"depth": 5,
|
||||
@@ -147,10 +169,14 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -165,7 +191,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -201,7 +227,9 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
child1, child2 = factories.DocumentFactory.create_batch(
|
||||
2, parent=document, link_reach="restricted"
|
||||
)
|
||||
grand_child = factories.DocumentFactory(parent=child1)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
@@ -217,6 +245,10 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -231,10 +263,14 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"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),
|
||||
"depth": 3,
|
||||
@@ -249,10 +285,14 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": document.link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -267,7 +307,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -289,7 +329,9 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
grand_parent = factories.DocumentFactory(link_reach=reach)
|
||||
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
|
||||
document = factories.DocumentFactory(link_reach="restricted", parent=parent)
|
||||
child1, child2 = factories.DocumentFactory.create_batch(2, parent=document)
|
||||
child1, child2 = factories.DocumentFactory.create_batch(
|
||||
2, parent=document, link_reach="restricted"
|
||||
)
|
||||
grand_child = factories.DocumentFactory(parent=child1)
|
||||
|
||||
factories.UserDocumentAccessFactory(document=child1)
|
||||
@@ -304,6 +346,10 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -318,10 +364,14 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"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),
|
||||
"depth": 5,
|
||||
@@ -336,10 +386,14 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -354,7 +408,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -414,6 +468,10 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -428,10 +486,14 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": grand_child.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"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),
|
||||
"depth": 3,
|
||||
@@ -446,10 +508,14 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -464,7 +530,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -504,6 +570,10 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 4,
|
||||
@@ -518,10 +588,14 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": grand_child.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"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),
|
||||
"depth": 5,
|
||||
@@ -536,10 +610,14 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 4,
|
||||
@@ -554,7 +632,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [grand_parent_access.role],
|
||||
"user_role": grand_parent_access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -640,6 +718,10 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"results": [
|
||||
{
|
||||
"abilities": child1.get_abilities(user),
|
||||
"ancestors_link_reach": child1.ancestors_link_reach,
|
||||
"ancestors_link_role": child1.ancestors_link_role,
|
||||
"computed_link_reach": child1.computed_link_reach,
|
||||
"computed_link_role": child1.computed_link_role,
|
||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -654,10 +736,14 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"path": child1.path,
|
||||
"title": child1.title,
|
||||
"updated_at": child1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": grand_child.get_abilities(user),
|
||||
"ancestors_link_reach": grand_child.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_child.ancestors_link_role,
|
||||
"computed_link_reach": grand_child.computed_link_reach,
|
||||
"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),
|
||||
"depth": 3,
|
||||
@@ -672,10 +758,14 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"path": grand_child.path,
|
||||
"title": grand_child.title,
|
||||
"updated_at": grand_child.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": child2.get_abilities(user),
|
||||
"ancestors_link_reach": child2.ancestors_link_reach,
|
||||
"ancestors_link_role": child2.ancestors_link_role,
|
||||
"computed_link_reach": child2.computed_link_reach,
|
||||
"computed_link_role": child2.computed_link_role,
|
||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(child2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -690,7 +780,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
||||
"path": child2.path,
|
||||
"title": child2.title,
|
||||
"updated_at": child2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ from django.utils import timezone
|
||||
import pycrdt
|
||||
import pytest
|
||||
import requests
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
@@ -60,7 +61,7 @@ def test_api_documents_duplicate_forbidden():
|
||||
def test_api_documents_duplicate_anonymous():
|
||||
"""Anonymous users should not be able to duplicate documents even with read access."""
|
||||
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
document = factories.DocumentFactory(link_reach="public", link_role="reader")
|
||||
|
||||
response = APIClient().post(f"/api/v1.0/documents/{document.id!s}/duplicate/")
|
||||
|
||||
@@ -133,19 +134,21 @@ def test_api_documents_duplicate_success(index):
|
||||
|
||||
# Ensure access persists after the owner loses access to the original document
|
||||
models.DocumentAccess.objects.filter(document=document).delete()
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=image_refs[0][1]
|
||||
)
|
||||
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=image_refs[0][1]
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
authorization = response["Authorization"]
|
||||
assert "AWS4-HMAC-SHA256 Credential=" in authorization
|
||||
assert (
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
response = requests.get(
|
||||
@@ -168,14 +171,17 @@ def test_api_documents_duplicate_success(index):
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_api_documents_duplicate_with_accesses():
|
||||
"""Accesses should be duplicated if the user requests it specifically."""
|
||||
@pytest.mark.parametrize("role", ["owner", "administrator"])
|
||||
def test_api_documents_duplicate_with_accesses_admin(role):
|
||||
"""
|
||||
Accesses should be duplicated if the user requests it specifically and is owner or admin.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(
|
||||
users=[user],
|
||||
users=[(user, role)],
|
||||
title="document with accesses",
|
||||
)
|
||||
user_access = factories.UserDocumentAccessFactory(document=document)
|
||||
@@ -205,3 +211,85 @@ def test_api_documents_duplicate_with_accesses():
|
||||
assert duplicated_accesses.get(user=user).role == "owner"
|
||||
assert duplicated_accesses.get(user=user_access.user).role == user_access.role
|
||||
assert duplicated_accesses.get(team=team_access.team).role == team_access.role
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["editor", "reader"])
|
||||
def test_api_documents_duplicate_with_accesses_non_admin(role):
|
||||
"""
|
||||
Accesses should not be duplicated if the user requests it specifically and is not owner
|
||||
or admin.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(
|
||||
users=[(user, role)],
|
||||
title="document with accesses",
|
||||
)
|
||||
factories.UserDocumentAccessFactory(document=document)
|
||||
factories.TeamDocumentAccessFactory(document=document)
|
||||
|
||||
# Duplicate the document via the API endpoint requesting to duplicate accesses
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/duplicate/",
|
||||
{"with_accesses": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
duplicated_document = models.Document.objects.get(id=response.json()["id"])
|
||||
assert duplicated_document.title == "Copy of document with accesses"
|
||||
assert duplicated_document.content == document.content
|
||||
assert duplicated_document.link_reach == document.link_reach
|
||||
assert duplicated_document.link_role == document.link_role
|
||||
assert duplicated_document.creator == user
|
||||
assert duplicated_document.duplicated_from == document
|
||||
assert duplicated_document.attachments == []
|
||||
|
||||
# Check that accesses were duplicated and the user who did the duplicate is forced as owner
|
||||
duplicated_accesses = duplicated_document.accesses
|
||||
assert duplicated_accesses.count() == 1
|
||||
assert duplicated_accesses.get(user=user).role == "owner"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["editor", "reader"])
|
||||
def test_api_documents_duplicate_non_root_document(role):
|
||||
"""
|
||||
Non-root documents can be duplicated but without accesses.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
child = factories.DocumentFactory(
|
||||
parent=document, users=[(user, role)], title="document with accesses"
|
||||
)
|
||||
|
||||
assert child.accesses.count() == 1
|
||||
|
||||
# Duplicate the document via the API endpoint requesting to duplicate accesses
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{child.id!s}/duplicate/",
|
||||
{"with_accesses": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
duplicated_document = models.Document.objects.get(id=response.json()["id"])
|
||||
assert duplicated_document.title == "Copy of document with accesses"
|
||||
assert duplicated_document.content == child.content
|
||||
assert duplicated_document.link_reach == child.link_reach
|
||||
assert duplicated_document.link_role == child.link_role
|
||||
assert duplicated_document.creator == user
|
||||
assert duplicated_document.duplicated_from == child
|
||||
assert duplicated_document.attachments == []
|
||||
|
||||
# No access should be created for non root documents
|
||||
duplicated_accesses = duplicated_document.accesses
|
||||
assert duplicated_accesses.count() == 0
|
||||
assert duplicated_document.is_sibling_of(child)
|
||||
assert duplicated_document.is_child_of(document)
|
||||
|
||||
@@ -45,7 +45,10 @@ def test_api_document_favorite_anonymous_user(method, reach):
|
||||
],
|
||||
)
|
||||
def test_api_document_favorite_authenticated_post_allowed(reach, has_role):
|
||||
"""Authenticated users should be able to mark a document as favorite using POST."""
|
||||
"""
|
||||
Authenticated users should be able to mark a document to which they have access
|
||||
as favorite using POST.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
client = APIClient()
|
||||
@@ -69,7 +72,10 @@ def test_api_document_favorite_authenticated_post_allowed(reach, has_role):
|
||||
|
||||
|
||||
def test_api_document_favorite_authenticated_post_forbidden():
|
||||
"""Authenticated users should be able to mark a document as favorite using POST."""
|
||||
"""
|
||||
Authenticated users should not be allowed to mark a document to which they don't
|
||||
have access as favorite using POST.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory(link_reach="restricted")
|
||||
client = APIClient()
|
||||
|
||||
@@ -41,8 +41,8 @@ def test_api_document_favorite_list_authenticated_with_favorite():
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# User don't have access to this document, let say it had access and this access has been
|
||||
# removed. It should not be in the favorite list anymore.
|
||||
# If the user doesn't have access to this document (e.g the user had access
|
||||
# and this access was removed), it should not be in the favorite list anymore.
|
||||
factories.DocumentFactory(favorited_by=[user])
|
||||
|
||||
document = factories.UserDocumentAccessFactory(
|
||||
@@ -59,6 +59,10 @@ def test_api_document_favorite_list_authenticated_with_favorite():
|
||||
"results": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"content": document.content,
|
||||
@@ -74,7 +78,7 @@ def test_api_document_favorite_list_authenticated_with_favorite():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": ["reader"],
|
||||
"user_role": "reader",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ def test_api_documents_list_format():
|
||||
assert results[0] == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
@@ -76,7 +80,7 @@ def test_api_documents_list_format():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -148,11 +152,11 @@ def test_api_documents_list_authenticated_direct(django_assert_num_queries):
|
||||
str(child4_with_access.id),
|
||||
}
|
||||
|
||||
with django_assert_num_queries(12):
|
||||
with django_assert_num_queries(14):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
# nb_accesses should now be cached
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(6):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -268,11 +272,11 @@ def test_api_documents_list_authenticated_link_reach_public_or_authenticated(
|
||||
|
||||
expected_ids = {str(document1.id), str(document2.id), str(visible_child.id)}
|
||||
|
||||
with django_assert_num_queries(10):
|
||||
with django_assert_num_queries(11):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
# nb_accesses should now be cached
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(5):
|
||||
response = client.get("/api/v1.0/documents/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -312,6 +312,84 @@ def test_api_documents_list_filter_is_favorite_invalid():
|
||||
assert len(results) == 5
|
||||
|
||||
|
||||
# Filters: is_masked
|
||||
|
||||
|
||||
def test_api_documents_list_filter_is_masked_true():
|
||||
"""
|
||||
Authenticated users should be able to filter documents they marked as masked.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.DocumentFactory.create_batch(2, users=[user])
|
||||
masked_documents = factories.DocumentFactory.create_batch(
|
||||
3, users=[user], masked_by=[user]
|
||||
)
|
||||
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
|
||||
for document in unmasked_documents:
|
||||
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
|
||||
|
||||
response = client.get("/api/v1.0/documents/?is_masked=true")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
assert len(results) == 3
|
||||
|
||||
# Ensure all results are marked as masked by the current user
|
||||
masked_documents_ids = [str(doc.id) for doc in masked_documents]
|
||||
for result in results:
|
||||
assert result["id"] in masked_documents_ids
|
||||
|
||||
|
||||
def test_api_documents_list_filter_is_masked_false():
|
||||
"""
|
||||
Authenticated users should be able to filter documents they didn't mark as masked.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.DocumentFactory.create_batch(2, users=[user])
|
||||
masked_documents = factories.DocumentFactory.create_batch(
|
||||
3, users=[user], masked_by=[user]
|
||||
)
|
||||
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
|
||||
for document in unmasked_documents:
|
||||
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
|
||||
|
||||
response = client.get("/api/v1.0/documents/?is_masked=false")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
assert len(results) == 4
|
||||
|
||||
# Ensure all results are not marked as masked by the current user
|
||||
masked_documents_ids = [str(doc.id) for doc in masked_documents]
|
||||
for result in results:
|
||||
assert result["id"] not in masked_documents_ids
|
||||
|
||||
|
||||
def test_api_documents_list_filter_is_masked_invalid():
|
||||
"""Filtering with an invalid `is_masked` value should do nothing."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.DocumentFactory.create_batch(2, users=[user])
|
||||
factories.DocumentFactory.create_batch(3, users=[user], masked_by=[user])
|
||||
unmasked_documents = factories.DocumentFactory.create_batch(2, users=[user])
|
||||
for document in unmasked_documents:
|
||||
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
|
||||
|
||||
response = client.get("/api/v1.0/documents/?is_masked=invalid")
|
||||
|
||||
assert response.status_code == 200
|
||||
results = response.json()["results"]
|
||||
assert len(results) == 7
|
||||
|
||||
|
||||
# Filters: title
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
"""Test mask document API endpoint for users in impress's core app."""
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach",
|
||||
[
|
||||
"restricted",
|
||||
"authenticated",
|
||||
"public",
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("method", ["post", "delete"])
|
||||
def test_api_document_mask_anonymous_user(method, reach):
|
||||
"""Anonymous users should not be able to mask/unmask documents."""
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
|
||||
response = getattr(APIClient(), method)(
|
||||
f"/api/v1.0/documents/{document.id!s}/mask/"
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
# Verify in database
|
||||
assert models.LinkTrace.objects.exists() is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, has_role",
|
||||
[
|
||||
["restricted", True],
|
||||
["authenticated", False],
|
||||
["authenticated", True],
|
||||
["public", False],
|
||||
["public", True],
|
||||
],
|
||||
)
|
||||
def test_api_document_mask_authenticated_post_allowed(reach, has_role):
|
||||
"""Authenticated users should be able to mask a document to which they have access."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
if has_role:
|
||||
models.DocumentAccess.objects.create(document=document, user=user)
|
||||
|
||||
# Try masking the document without a link trace
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "User never accessed this document before."}
|
||||
assert not models.LinkTrace.objects.filter(document=document, user=user).exists()
|
||||
|
||||
models.LinkTrace.objects.create(document=document, user=user)
|
||||
# Mask document
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json() == {"detail": "Document was masked"}
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=True
|
||||
).exists()
|
||||
|
||||
|
||||
def test_api_document_mask_authenticated_post_forbidden():
|
||||
"""
|
||||
Authenticated users should no be allowed to mask a document
|
||||
to which they don't have access.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach="restricted")
|
||||
|
||||
# Try masking
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
# Verify in database
|
||||
assert (
|
||||
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, has_role",
|
||||
[
|
||||
["restricted", True],
|
||||
["authenticated", False],
|
||||
["authenticated", True],
|
||||
["public", False],
|
||||
["public", True],
|
||||
],
|
||||
)
|
||||
def test_api_document_mask_authenticated_post_already_masked_allowed(reach, has_role):
|
||||
"""POST should not create duplicate link trace if already marked."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach, masked_by=[user])
|
||||
if has_role:
|
||||
models.DocumentAccess.objects.create(document=document, user=user)
|
||||
|
||||
# Try masking again
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"detail": "Document was already masked"}
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=True
|
||||
).exists()
|
||||
|
||||
|
||||
def test_api_document_mask_authenticated_post_already_masked_forbidden():
|
||||
"""POST should not create duplicate masks if already marked."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach="restricted", masked_by=[user])
|
||||
# Try masking again
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
assert models.LinkTrace.objects.filter(document=document, user=user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, has_role",
|
||||
[
|
||||
["restricted", True],
|
||||
["authenticated", False],
|
||||
["authenticated", True],
|
||||
["public", False],
|
||||
["public", True],
|
||||
],
|
||||
)
|
||||
def test_api_document_mask_authenticated_post_unmasked_allowed(reach, has_role):
|
||||
"""POST should not create duplicate link trace if unmasked."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
|
||||
if has_role:
|
||||
models.DocumentAccess.objects.create(document=document, user=user)
|
||||
|
||||
# Try masking again
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json() == {"detail": "Document was masked"}
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=True
|
||||
).exists()
|
||||
|
||||
|
||||
def test_api_document_mask_authenticated_post_unmasked_forbidden():
|
||||
"""POST should not create duplicate masks if unmasked."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach="restricted")
|
||||
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
|
||||
# Try masking again
|
||||
response = client.post(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=False
|
||||
).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, has_role",
|
||||
[
|
||||
["restricted", True],
|
||||
["authenticated", False],
|
||||
["authenticated", True],
|
||||
["public", False],
|
||||
["public", True],
|
||||
],
|
||||
)
|
||||
def test_api_document_mask_authenticated_delete_allowed(reach, has_role):
|
||||
"""Authenticated users should be able to unmask a document using DELETE."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach, masked_by=[user])
|
||||
if has_role:
|
||||
models.DocumentAccess.objects.create(document=document, user=user)
|
||||
|
||||
# Unmask document
|
||||
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 204
|
||||
assert response.content == b"" # No body
|
||||
assert response.text == "" # Empty decoded text
|
||||
assert "Content-Type" not in response.headers # No Content-Type for 204
|
||||
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=False
|
||||
).exists()
|
||||
|
||||
|
||||
def test_api_document_mask_authenticated_delete_forbidden():
|
||||
"""
|
||||
Authenticated users should not be allowed to unmask a document if
|
||||
they don't have access to it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach="restricted", masked_by=[user])
|
||||
|
||||
# Unmask document
|
||||
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=True
|
||||
).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, has_role",
|
||||
[
|
||||
["restricted", True],
|
||||
["authenticated", False],
|
||||
["authenticated", True],
|
||||
["public", False],
|
||||
["public", True],
|
||||
],
|
||||
)
|
||||
def test_api_document_mask_authenticated_delete_not_masked_allowed(reach, has_role):
|
||||
"""DELETE should be idempotent if the document is not masked."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
if has_role:
|
||||
models.DocumentAccess.objects.create(document=document, user=user)
|
||||
|
||||
# Try unmasking the document without a link trace
|
||||
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"detail": "User never accessed this document before."}
|
||||
assert not models.LinkTrace.objects.filter(document=document, user=user).exists()
|
||||
|
||||
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
|
||||
# Unmask document
|
||||
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"detail": "Document was already not masked"}
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=False
|
||||
).exists()
|
||||
|
||||
|
||||
def test_api_document_mask_authenticated_delete_not_masked_forbidden():
|
||||
"""DELETE should be idempotent if the document is not masked."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach="restricted")
|
||||
|
||||
# Try to unmask when no entry exists
|
||||
response = client.delete(f"/api/v1.0/documents/{document.id!s}/mask/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
assert (
|
||||
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reach, has_role",
|
||||
[
|
||||
["restricted", True],
|
||||
["authenticated", False],
|
||||
["authenticated", True],
|
||||
["public", False],
|
||||
["public", True],
|
||||
],
|
||||
)
|
||||
def test_api_document_mask_authenticated_post_unmark_then_mark_again_allowed(
|
||||
reach, has_role
|
||||
):
|
||||
"""A user should be able to mask, unmask, and mask a document again."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
if has_role:
|
||||
models.DocumentAccess.objects.create(document=document, user=user)
|
||||
models.LinkTrace.objects.create(document=document, user=user, is_masked=False)
|
||||
|
||||
url = f"/api/v1.0/documents/{document.id!s}/mask/"
|
||||
|
||||
# Mask document
|
||||
response = client.post(url)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Unmask document
|
||||
response = client.delete(url)
|
||||
assert response.status_code == 204
|
||||
assert response.content == b"" # No body
|
||||
assert response.text == "" # Empty decoded text
|
||||
assert "Content-Type" not in response.headers # No Content-Type for 204
|
||||
|
||||
# Mask document again
|
||||
response = client.post(url)
|
||||
assert response.status_code == 201
|
||||
assert response.json() == {"detail": "Document was masked"}
|
||||
|
||||
assert models.LinkTrace.objects.filter(
|
||||
document=document, user=user, is_masked=True
|
||||
).exists()
|
||||
@@ -12,6 +12,7 @@ from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
@@ -52,9 +53,11 @@ def test_api_documents_media_auth_anonymous_public():
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -64,7 +67,7 @@ def test_api_documents_media_auth_anonymous_public():
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -167,9 +170,11 @@ def test_api_documents_media_auth_anonymous_attachments():
|
||||
parent = factories.DocumentFactory(link_reach="public")
|
||||
factories.DocumentFactory(parent=parent, link_reach="restricted", attachments=[key])
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -179,7 +184,7 @@ def test_api_documents_media_auth_anonymous_attachments():
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -221,9 +226,11 @@ def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach=reach, attachments=[key])
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -233,7 +240,7 @@ def test_api_documents_media_auth_authenticated_public_or_authenticated(reach):
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -307,9 +314,11 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(document=document, team="lasuite")
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
now = timezone.now()
|
||||
with freeze_time(now):
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=media_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -319,7 +328,7 @@ def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
@@ -373,10 +382,12 @@ def test_api_documents_media_auth_missing_status_metadata():
|
||||
|
||||
factories.DocumentFactory(id=document_id, link_reach="public", attachments=[key])
|
||||
|
||||
now = timezone.now()
|
||||
original_url = f"http://localhost/media/{key:s}"
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
with freeze_time(now):
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -386,7 +397,7 @@ def test_api_documents_media_auth_missing_status_metadata():
|
||||
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature="
|
||||
in authorization
|
||||
)
|
||||
assert response["X-Amz-Date"] == timezone.now().strftime("%Y%m%dT%H%M%SZ")
|
||||
assert response["X-Amz-Date"] == now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
s3_url = urlparse(settings.AWS_S3_ENDPOINT_URL)
|
||||
file_url = f"{settings.AWS_S3_ENDPOINT_URL:s}/impress-media-storage/{key:s}"
|
||||
|
||||
@@ -124,8 +124,8 @@ def test_api_documents_move_authenticated_target_roles_mocked(
|
||||
target_role, target_parent_role, position
|
||||
):
|
||||
"""
|
||||
Authenticated users with insufficient permissions on the target document (or its
|
||||
parent depending on the position chosen), should not be allowed to move documents.
|
||||
Only authenticated users with sufficient permissions on the target document (or its
|
||||
parent depending on the position chosen), should be allowed to move documents.
|
||||
"""
|
||||
|
||||
user = factories.UserFactory()
|
||||
@@ -208,6 +208,107 @@ def test_api_documents_move_authenticated_target_roles_mocked(
|
||||
assert document.is_root() is True
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_no_owner_user_and_team():
|
||||
"""
|
||||
Moving a document with no owner to the root of the tree should automatically declare
|
||||
the owner of the previous root of the document as owner of the document itself.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent_owner = factories.UserFactory()
|
||||
parent = factories.DocumentFactory(
|
||||
users=[(parent_owner, "owner")], teams=[("lasuite", "owner")]
|
||||
)
|
||||
# A document with no owner
|
||||
document = factories.DocumentFactory(parent=parent, users=[(user, "administrator")])
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
target = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": "first-sibling"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Document moved successfully."}
|
||||
assert list(target.get_siblings()) == [document, parent, target]
|
||||
|
||||
document.refresh_from_db()
|
||||
assert list(document.get_children()) == [child]
|
||||
assert document.accesses.count() == 3
|
||||
assert document.accesses.get(user__isnull=False, role="owner").user == parent_owner
|
||||
assert document.accesses.get(user__isnull=True, role="owner").team == "lasuite"
|
||||
assert document.accesses.get(role="administrator").user == user
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_no_owner_same_user():
|
||||
"""
|
||||
Moving a document should not fail if the user moving a document with no owner was
|
||||
at the same time owner of the previous root and has a role on the document being moved.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(
|
||||
users=[(user, "owner")], teams=[("lasuite", "owner")]
|
||||
)
|
||||
# A document with no owner
|
||||
document = factories.DocumentFactory(parent=parent, users=[(user, "reader")])
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
target = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": "first-sibling"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Document moved successfully."}
|
||||
assert list(target.get_siblings()) == [document, parent, target]
|
||||
|
||||
document.refresh_from_db()
|
||||
assert list(document.get_children()) == [child]
|
||||
assert document.accesses.count() == 2
|
||||
assert document.accesses.get(user__isnull=False, role="owner").user == user
|
||||
assert document.accesses.get(user__isnull=True, role="owner").team == "lasuite"
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_no_owner_same_team():
|
||||
"""
|
||||
Moving a document should not fail if the team that is owner of the document root was
|
||||
already declared on the document with a different role.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
parent = factories.DocumentFactory(teams=[("lasuite", "owner")])
|
||||
# A document with no owner but same team
|
||||
document = factories.DocumentFactory(
|
||||
parent=parent, users=[(user, "administrator")], teams=[("lasuite", "reader")]
|
||||
)
|
||||
child = factories.DocumentFactory(parent=document)
|
||||
target = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": "first-sibling"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Document moved successfully."}
|
||||
assert list(target.get_siblings()) == [document, parent, target]
|
||||
|
||||
document.refresh_from_db()
|
||||
assert list(document.get_children()) == [child]
|
||||
assert document.accesses.count() == 2
|
||||
assert document.accesses.get(user__isnull=False, role="administrator").user == user
|
||||
assert document.accesses.get(user__isnull=True, role="owner").team == "lasuite"
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_deleted_document():
|
||||
"""
|
||||
It should not be possible to move a deleted document or its descendants, even
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: retrieve
|
||||
"""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import random
|
||||
from datetime import timedelta
|
||||
@@ -11,7 +12,7 @@ from django.utils import timezone
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core import choices, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -31,13 +32,15 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": document.link_role == "editor",
|
||||
"can_edit": document.link_role == "editor",
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"descendants": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"duplicate": False,
|
||||
# Anonymous user can't favorite a document even with read access
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
@@ -45,8 +48,9 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": False,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -59,6 +63,10 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -73,7 +81,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +99,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
|
||||
assert response.status_code == 200
|
||||
links = document.get_ancestors().values("link_reach", "link_role")
|
||||
links_definition = choices.get_equivalent_link_definition(links)
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
@@ -99,18 +108,23 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": grand_parent.link_role == "editor",
|
||||
"can_edit": grand_parent.link_role == "editor",
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"duplicate": False,
|
||||
# Anonymous user can't favorite a document even with read access
|
||||
"favorite": False,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(links),
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(
|
||||
**links_definition
|
||||
),
|
||||
"mask": False,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -123,6 +137,10 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": "public",
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": "public",
|
||||
"computed_link_role": grand_parent.link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -137,7 +155,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -196,11 +214,13 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"ai_transform": document.link_role == "editor",
|
||||
"ai_translate": document.link_role == "editor",
|
||||
"attachment_upload": document.link_role == "editor",
|
||||
"can_edit": document.link_role == "editor",
|
||||
"children_create": document.link_role == "editor",
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
@@ -209,8 +229,9 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -223,6 +244,10 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -237,7 +262,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
assert (
|
||||
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
|
||||
@@ -263,6 +288,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
|
||||
assert response.status_code == 200
|
||||
links = document.get_ancestors().values("link_reach", "link_role")
|
||||
links_definition = choices.get_equivalent_link_definition(links)
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
@@ -271,20 +297,25 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"ai_transform": grand_parent.link_role == "editor",
|
||||
"ai_translate": grand_parent.link_role == "editor",
|
||||
"attachment_upload": grand_parent.link_role == "editor",
|
||||
"can_edit": grand_parent.link_role == "editor",
|
||||
"children_create": grand_parent.link_role == "editor",
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(links),
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(
|
||||
**links_definition
|
||||
),
|
||||
"mask": True,
|
||||
"move": False,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
"partial_update": grand_parent.link_role == "editor",
|
||||
"restore": False,
|
||||
"retrieve": True,
|
||||
@@ -294,6 +325,10 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": reach,
|
||||
"ancestors_link_role": grand_parent.link_role,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -308,7 +343,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +439,10 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"creator": str(document.creator.id),
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -418,7 +457,7 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +483,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
)
|
||||
assert response.status_code == 200
|
||||
links = document.get_ancestors().values("link_reach", "link_role")
|
||||
link_definition = choices.get_equivalent_link_definition(links)
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": {
|
||||
@@ -452,17 +492,22 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"ai_transform": access.role != "reader",
|
||||
"ai_translate": access.role != "reader",
|
||||
"attachment_upload": access.role != "reader",
|
||||
"can_edit": access.role != "reader",
|
||||
"children_create": access.role != "reader",
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": access.role == "owner",
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
"invite_owner": access.role == "owner",
|
||||
"link_configuration": access.role in ["administrator", "owner"],
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(links),
|
||||
"link_select_options": models.LinkReachChoices.get_select_options(
|
||||
**link_definition
|
||||
),
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": access.role in ["administrator", "owner"],
|
||||
@@ -475,6 +520,10 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"versions_list": True,
|
||||
"versions_retrieve": True,
|
||||
},
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": "restricted",
|
||||
"computed_link_role": None,
|
||||
"content": document.content,
|
||||
"creator": str(document.creator.id),
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -489,7 +538,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -585,16 +634,16 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_teams)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"teams,roles",
|
||||
"teams,role",
|
||||
[
|
||||
[["readers"], ["reader"]],
|
||||
[["unknown", "readers"], ["reader"]],
|
||||
[["editors"], ["editor"]],
|
||||
[["unknown", "editors"], ["editor"]],
|
||||
[["readers"], "reader"],
|
||||
[["unknown", "readers"], "reader"],
|
||||
[["editors"], "editor"],
|
||||
[["unknown", "editors"], "editor"],
|
||||
],
|
||||
)
|
||||
def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
teams, roles, mock_user_teams
|
||||
teams, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a document to which they
|
||||
@@ -627,6 +676,10 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -641,20 +694,20 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": roles,
|
||||
"user_role": role,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"teams,roles",
|
||||
"teams,role",
|
||||
[
|
||||
[["administrators"], ["administrator"]],
|
||||
[["editors", "administrators"], ["administrator", "editor"]],
|
||||
[["unknown", "administrators"], ["administrator"]],
|
||||
[["administrators"], "administrator"],
|
||||
[["editors", "administrators"], "administrator"],
|
||||
[["unknown", "administrators"], "administrator"],
|
||||
],
|
||||
)
|
||||
def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
teams, roles, mock_user_teams
|
||||
teams, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a document to which they
|
||||
@@ -689,6 +742,10 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -703,21 +760,21 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": roles,
|
||||
"user_role": role,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"teams,roles",
|
||||
"teams,role",
|
||||
[
|
||||
[["owners"], ["owner"]],
|
||||
[["owners", "administrators"], ["owner", "administrator"]],
|
||||
[["members", "administrators", "owners"], ["owner", "administrator"]],
|
||||
[["unknown", "owners"], ["owner"]],
|
||||
[["owners"], "owner"],
|
||||
[["owners", "administrators"], "owner"],
|
||||
[["members", "administrators", "owners"], "owner"],
|
||||
[["unknown", "owners"], "owner"],
|
||||
],
|
||||
)
|
||||
def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
teams, roles, mock_user_teams
|
||||
teams, role, mock_user_teams
|
||||
):
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a restricted document to which
|
||||
@@ -751,6 +808,10 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
@@ -765,11 +826,11 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": roles,
|
||||
"user_role": role,
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_retrieve_user_roles(django_assert_max_num_queries):
|
||||
def test_api_documents_retrieve_user_role(django_assert_max_num_queries):
|
||||
"""
|
||||
Roles should be annotated on querysets taking into account all documents ancestors.
|
||||
"""
|
||||
@@ -792,15 +853,14 @@ def test_api_documents_retrieve_user_roles(django_assert_max_num_queries):
|
||||
factories.UserDocumentAccessFactory(document=parent, user=user),
|
||||
factories.UserDocumentAccessFactory(document=document, user=user),
|
||||
)
|
||||
expected_roles = {access.role for access in accesses}
|
||||
expected_role = choices.RoleChoices.max(*[access.role for access in accesses])
|
||||
|
||||
with django_assert_max_num_queries(14):
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
user_roles = response.json()["user_roles"]
|
||||
assert set(user_roles) == expected_roles
|
||||
assert response.json()["user_role"] == expected_role
|
||||
|
||||
|
||||
def test_api_documents_retrieve_numqueries_with_link_trace(django_assert_num_queries):
|
||||
|
||||
@@ -75,11 +75,13 @@ def test_api_documents_trashbin_format():
|
||||
"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,
|
||||
@@ -88,8 +90,9 @@ def test_api_documents_trashbin_format():
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False, # Can't move a deleted document
|
||||
@@ -102,6 +105,10 @@ def test_api_documents_trashbin_format():
|
||||
"versions_list": True,
|
||||
"versions_retrieve": True,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 1,
|
||||
@@ -114,7 +121,7 @@ def test_api_documents_trashbin_format():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": ["owner"],
|
||||
"user_role": "owner",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -32,13 +32,19 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(AnonymousUser()),
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -57,9 +63,13 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -74,11 +84,15 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": sibling1.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": sibling1.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling1.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling1.computed_link_reach,
|
||||
"computed_link_role": sibling1.computed_link_role,
|
||||
"created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling1.creator.id),
|
||||
"depth": 2,
|
||||
@@ -93,11 +107,15 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": sibling1.path,
|
||||
"title": sibling1.title,
|
||||
"updated_at": sibling1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": sibling2.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": sibling2.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling2.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling2.computed_link_reach,
|
||||
"computed_link_role": sibling2.computed_link_role,
|
||||
"created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling2.creator.id),
|
||||
"depth": 2,
|
||||
@@ -112,9 +130,11 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": sibling2.path,
|
||||
"title": sibling2.title,
|
||||
"updated_at": sibling2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -129,7 +149,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -163,18 +183,28 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
expected_tree = {
|
||||
"abilities": grand_parent.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": grand_parent.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": parent.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -193,9 +223,11 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -214,11 +246,15 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": document.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": document_sibling.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": document_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": document_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": document_sibling.computed_link_reach,
|
||||
"computed_link_role": document_sibling.computed_link_role,
|
||||
"created_at": document_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -237,9 +273,11 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": document_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
@@ -254,11 +292,15 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": parent_sibling.get_abilities(AnonymousUser()),
|
||||
"ancestors_link_reach": parent_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": parent_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": parent_sibling.computed_link_reach,
|
||||
"computed_link_role": parent_sibling.computed_link_role,
|
||||
"created_at": parent_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -277,9 +319,11 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"updated_at": parent_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": grand_parent.computed_link_reach,
|
||||
"computed_link_role": grand_parent.computed_link_role,
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
@@ -294,8 +338,9 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
||||
"path": grand_parent.path,
|
||||
"title": grand_parent.title,
|
||||
"updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
assert response.json() == expected_tree
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", ["restricted", "authenticated"])
|
||||
@@ -341,13 +386,21 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -366,9 +419,11 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -383,11 +438,15 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": sibling.get_abilities(user),
|
||||
"ancestors_link_reach": sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling.computed_link_reach,
|
||||
"computed_link_role": sibling.computed_link_role,
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
@@ -402,9 +461,11 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"path": sibling.path,
|
||||
"title": sibling.title,
|
||||
"updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -419,7 +480,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -460,16 +521,26 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": grand_parent.get_abilities(user),
|
||||
"ancestors_link_reach": grand_parent.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -488,9 +559,11 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -509,11 +582,15 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": document.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": document_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": document_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": document_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": document_sibling.computed_link_reach,
|
||||
"computed_link_role": document_sibling.computed_link_role,
|
||||
"created_at": document_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -532,9 +609,11 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": document_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
@@ -549,11 +628,15 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
{
|
||||
"abilities": parent_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": parent_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": parent_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": parent_sibling.computed_link_reach,
|
||||
"computed_link_role": parent_sibling.computed_link_role,
|
||||
"created_at": parent_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -572,9 +655,11 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"updated_at": parent_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": grand_parent.computed_link_reach,
|
||||
"computed_link_role": grand_parent.computed_link_role,
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
@@ -589,7 +674,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
||||
"path": grand_parent.path,
|
||||
"title": grand_parent.title,
|
||||
"updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [],
|
||||
"user_role": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -639,13 +724,21 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -664,9 +757,11 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -681,11 +776,15 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": sibling.get_abilities(user),
|
||||
"ancestors_link_reach": sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling.computed_link_reach,
|
||||
"computed_link_role": sibling.computed_link_role,
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
@@ -700,9 +799,11 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"path": sibling.path,
|
||||
"title": sibling.title,
|
||||
"updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -717,7 +818,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -762,16 +863,26 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": grand_parent.get_abilities(user),
|
||||
"ancestors_link_reach": grand_parent.ancestors_link_reach,
|
||||
"ancestors_link_role": grand_parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": parent.ancestors_link_reach,
|
||||
"ancestors_link_role": parent.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": document.ancestors_link_reach,
|
||||
"ancestors_link_role": document.ancestors_link_role,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": child.ancestors_link_reach,
|
||||
"ancestors_link_role": child.ancestors_link_role,
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"children": [],
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -790,9 +901,11 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -811,11 +924,15 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": document.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": document_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": document_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": document_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": document_sibling.computed_link_reach,
|
||||
"computed_link_role": document_sibling.computed_link_role,
|
||||
"created_at": document_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -834,9 +951,11 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": document_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 3,
|
||||
@@ -851,11 +970,15 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": parent_sibling.get_abilities(user),
|
||||
"ancestors_link_reach": parent_sibling.ancestors_link_reach,
|
||||
"ancestors_link_role": parent_sibling.ancestors_link_role,
|
||||
"children": [],
|
||||
"computed_link_reach": parent_sibling.computed_link_reach,
|
||||
"computed_link_role": parent_sibling.computed_link_role,
|
||||
"created_at": parent_sibling.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -874,9 +997,11 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"updated_at": parent_sibling.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": grand_parent.computed_link_reach,
|
||||
"computed_link_role": grand_parent.computed_link_role,
|
||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(grand_parent.creator.id),
|
||||
"depth": 2,
|
||||
@@ -891,7 +1016,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
||||
"path": grand_parent.path,
|
||||
"title": grand_parent.title,
|
||||
"updated_at": grand_parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
|
||||
@@ -949,13 +1074,21 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"abilities": parent.get_abilities(user),
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
"children": [
|
||||
{
|
||||
"abilities": document.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"children": [
|
||||
{
|
||||
"abilities": child.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"children": [],
|
||||
"computed_link_reach": child.computed_link_reach,
|
||||
"computed_link_role": child.computed_link_role,
|
||||
"created_at": child.created_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
@@ -974,9 +1107,11 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"updated_at": child.updated_at.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": document.computed_link_reach,
|
||||
"computed_link_role": document.computed_link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(document.creator.id),
|
||||
"depth": 2,
|
||||
@@ -991,11 +1126,15 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"path": document.path,
|
||||
"title": document.title,
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
{
|
||||
"abilities": sibling.get_abilities(user),
|
||||
"ancestors_link_reach": "restricted",
|
||||
"ancestors_link_role": None,
|
||||
"children": [],
|
||||
"computed_link_reach": sibling.computed_link_reach,
|
||||
"computed_link_role": sibling.computed_link_role,
|
||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(sibling.creator.id),
|
||||
"depth": 2,
|
||||
@@ -1010,9 +1149,11 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"path": sibling.path,
|
||||
"title": sibling.title,
|
||||
"updated_at": sibling.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
},
|
||||
],
|
||||
"computed_link_reach": parent.computed_link_reach,
|
||||
"computed_link_role": parent.computed_link_role,
|
||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"creator": str(parent.creator.id),
|
||||
"depth": 1,
|
||||
@@ -1027,5 +1168,5 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
||||
"path": parent.path,
|
||||
"title": parent.title,
|
||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"user_roles": [access.role],
|
||||
"user_role": access.role,
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ Tests for Documents API endpoint in impress's core app: update
|
||||
import random
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
@@ -44,6 +46,7 @@ def test_api_documents_update_anonymous_forbidden(reach, role, via_parent):
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -90,8 +93,9 @@ def test_api_documents_update_authenticated_unrelated_forbidden(
|
||||
|
||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
instance=factories.DocumentFactory(),
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -141,8 +145,9 @@ def test_api_documents_update_anonymous_or_authenticated_unrelated(
|
||||
|
||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
instance=factories.DocumentFactory(),
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -155,6 +160,10 @@ def test_api_documents_update_anonymous_or_authenticated_unrelated(
|
||||
for key, value in document_values.items():
|
||||
if key in [
|
||||
"id",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"accesses",
|
||||
"created_at",
|
||||
"creator",
|
||||
@@ -206,6 +215,7 @@ def test_api_documents_update_authenticated_reader(via, via_parent, mock_user_te
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -258,6 +268,7 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -270,6 +281,10 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
||||
for key, value in document_values.items():
|
||||
if key in [
|
||||
"id",
|
||||
"ancestors_link_reach",
|
||||
"ancestors_link_role",
|
||||
"computed_link_reach",
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"depth",
|
||||
@@ -287,6 +302,359 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
||||
assert value == new_document_values[key]
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_authenticated_no_websocket(settings):
|
||||
"""
|
||||
When a user updates the document, not connected to the websocket and is the first to update,
|
||||
the document should be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_authenticated_no_websocket_user_already_editing(settings):
|
||||
"""
|
||||
When a user updates the document, not connected to the websocket and is not the first to update,
|
||||
the document should not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "You are not allowed to edit this document."}
|
||||
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_no_websocket_other_user_connected_to_websocket(settings):
|
||||
"""
|
||||
When a user updates the document, not connected to the websocket and another user is connected
|
||||
to the websocket, the document should not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": False})
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "You are not allowed to edit this document."}
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_user_connected_to_websocket(settings):
|
||||
"""
|
||||
When a user updates the document, connected to the websocket, the document should be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": True})
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websocket(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the document should be updated like if the user was
|
||||
not connected to the websocket.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=500)
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the behavior fallback to the no websocket one.
|
||||
If an other user is already editing, the document should not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=500)
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_websocket_server_room_not_found_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the WebSocket server does not have the room created, the logic should fallback to
|
||||
no-WebSocket. If another user is already editing, the update must be denied.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=404)
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
assert ws_resp.call_count == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_force_websocket_param_to_true(settings):
|
||||
"""
|
||||
When the websocket parameter is set to true, the document should be updated without any check.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=500)
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
assert ws_resp.call_count == 0
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_documents_update_feature_flag_disabled(settings):
|
||||
"""
|
||||
When the feature flag is disabled, the document should be updated without any check.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = False
|
||||
endpoint_url = (
|
||||
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||
f"?room={document.id}&sessionKey={session_key}"
|
||||
)
|
||||
ws_resp = responses.get(endpoint_url, status=500)
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
assert ws_resp.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_teams):
|
||||
"""
|
||||
@@ -317,6 +685,7 @@ def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_t
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{other_document.id!s}/",
|
||||
new_document_values,
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_qu
|
||||
with django_assert_num_queries(11):
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys)},
|
||||
{"content": get_ydoc_with_mages(image_keys), "websocket": True},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -63,7 +63,7 @@ def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_qu
|
||||
with django_assert_num_queries(7):
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"content": get_ydoc_with_mages(image_keys[:2])},
|
||||
{"content": get_ydoc_with_mages(image_keys[:2]), "websocket": True},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -48,12 +48,7 @@ def test_api_template_accesses_list_authenticated_unrelated():
|
||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -96,8 +91,8 @@ def test_api_template_accesses_list_authenticated_related(via, mock_user_teams):
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 3
|
||||
assert sorted(content["results"], key=lambda x: x["id"]) == sorted(
|
||||
assert len(content) == 3
|
||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(user_access.id),
|
||||
|
||||
@@ -133,7 +133,7 @@ def test_api_template_accesses_create_authenticated_administrator(via, mock_user
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "Only owners of a resource can assign other users as owners."
|
||||
"detail": "Only owners of a template can assign other users as owners."
|
||||
}
|
||||
|
||||
# It should be allowed to create a lower access
|
||||
|
||||
@@ -186,7 +186,7 @@ def test_api_users_list_query_short_queries():
|
||||
"""
|
||||
Queries shorter than 5 characters should return an empty result set.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
user = factories.UserFactory(email="paul@example.com")
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
|
||||
@@ -123,16 +123,22 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_owner_of_self_last():
|
||||
def test_models_document_access_get_abilities_for_owner_of_self_last_on_root(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Check abilities of self access for the owner of a document when there is only one owner left.
|
||||
Check abilities of self access for the owner of a root document when there
|
||||
is only one owner left.
|
||||
"""
|
||||
access = factories.UserDocumentAccessFactory(role="owner")
|
||||
abilities = access.get_abilities(access.user)
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
abilities = access.get_abilities(access.user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
@@ -142,6 +148,28 @@ def test_models_document_access_get_abilities_for_owner_of_self_last():
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_owner_of_self_last_on_child(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""
|
||||
Check abilities of self access for the owner of a child document when there
|
||||
is only one owner left.
|
||||
"""
|
||||
parent = factories.DocumentFactory()
|
||||
access = factories.UserDocumentAccessFactory(document__parent=parent, role="owner")
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.get_abilities(access.user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_owner_of_owner():
|
||||
"""Check abilities of owner access for the owner of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="owner")
|
||||
@@ -155,7 +183,7 @@ def test_models_document_access_get_abilities_for_owner_of_owner():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +200,7 @@ def test_models_document_access_get_abilities_for_owner_of_administrator():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +217,7 @@ def test_models_document_access_get_abilities_for_owner_of_editor():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +234,7 @@ def test_models_document_access_get_abilities_for_owner_of_reader():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "editor"],
|
||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +271,7 @@ def test_models_document_access_get_abilities_for_administrator_of_administrator
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["editor", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator"],
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +288,7 @@ def test_models_document_access_get_abilities_for_administrator_of_editor():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "reader"],
|
||||
"set_role_to": ["reader", "editor", "administrator"],
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +305,7 @@ def test_models_document_access_get_abilities_for_administrator_of_reader():
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor"],
|
||||
"set_role_to": ["reader", "editor", "administrator"],
|
||||
}
|
||||
|
||||
|
||||
@@ -400,12 +428,12 @@ def test_models_document_access_get_abilities_for_reader_of_reader_user(
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_preset_role(django_assert_num_queries):
|
||||
"""No query is done if the role is preset, e.g., with a query annotation."""
|
||||
"""No query is done if user roles are preset on the document, e.g., with a query annotation."""
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="reader"
|
||||
).user
|
||||
access.user_roles = ["reader"]
|
||||
access.set_user_roles_tuple(None, "reader")
|
||||
|
||||
with django_assert_num_queries(0):
|
||||
abilities = access.get_abilities(user)
|
||||
|
||||
@@ -155,15 +155,18 @@ def test_models_documents_get_abilities_forbidden(
|
||||
"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,
|
||||
"mask": False,
|
||||
"media_auth": False,
|
||||
"media_check": False,
|
||||
"move": False,
|
||||
@@ -171,7 +174,7 @@ def test_models_documents_get_abilities_forbidden(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"partial_update": False,
|
||||
"restore": False,
|
||||
@@ -216,21 +219,24 @@ def test_models_documents_get_abilities_reader(
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"duplicate": is_authenticated,
|
||||
"favorite": is_authenticated,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": is_authenticated,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -252,7 +258,7 @@ def test_models_documents_get_abilities_reader(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -279,21 +285,24 @@ def test_models_documents_get_abilities_editor(
|
||||
"ai_transform": is_authenticated,
|
||||
"ai_translate": is_authenticated,
|
||||
"attachment_upload": True,
|
||||
"can_edit": True,
|
||||
"children_create": is_authenticated,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"duplicate": is_authenticated,
|
||||
"favorite": is_authenticated,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": is_authenticated,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -314,7 +323,7 @@ def test_models_documents_get_abilities_editor(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -331,11 +340,13 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"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,
|
||||
@@ -344,8 +355,9 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": True,
|
||||
@@ -380,11 +392,13 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
"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": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
@@ -393,8 +407,9 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": True,
|
||||
@@ -415,7 +430,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -432,11 +447,13 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"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": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
@@ -445,8 +462,9 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -467,7 +485,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -491,11 +509,13 @@ def test_models_documents_get_abilities_reader_user(
|
||||
"ai_transform": access_from_link and ai_access_setting != "restricted",
|
||||
"ai_translate": access_from_link and ai_access_setting != "restricted",
|
||||
"attachment_upload": access_from_link,
|
||||
"can_edit": access_from_link,
|
||||
"children_create": access_from_link,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
@@ -504,8 +524,9 @@ def test_models_documents_get_abilities_reader_user(
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -528,7 +549,7 @@ def test_models_documents_get_abilities_reader_user(
|
||||
assert all(
|
||||
value is False
|
||||
for key, value in document.get_abilities(user).items()
|
||||
if key != "link_select_options"
|
||||
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||
)
|
||||
|
||||
|
||||
@@ -548,11 +569,13 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
"descendants": True,
|
||||
"cors_proxy": True,
|
||||
"content": True,
|
||||
"destroy": False,
|
||||
"duplicate": True,
|
||||
"favorite": True,
|
||||
@@ -561,8 +584,9 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"link_select_options": {
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
"mask": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"move": False,
|
||||
@@ -1176,184 +1200,134 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ancestors_links, select_options",
|
||||
"reach, role, select_options",
|
||||
[
|
||||
# One ancestor
|
||||
(
|
||||
[{"link_reach": "public", "link_role": "reader"}],
|
||||
"public",
|
||||
"reader",
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
("public", "editor", {"public": ["editor"]}),
|
||||
(
|
||||
"authenticated",
|
||||
"reader",
|
||||
{
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
"authenticated",
|
||||
"editor",
|
||||
{"authenticated": ["editor"], "public": ["editor"]},
|
||||
),
|
||||
(
|
||||
"restricted",
|
||||
"reader",
|
||||
{
|
||||
"restricted": None,
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
"restricted",
|
||||
"editor",
|
||||
{
|
||||
"restricted": None,
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
"public": ["editor"],
|
||||
},
|
||||
),
|
||||
([{"link_reach": "public", "link_role": "editor"}], {"public": ["editor"]}),
|
||||
# Edge cases
|
||||
(
|
||||
[{"link_reach": "authenticated", "link_role": "reader"}],
|
||||
"public",
|
||||
None,
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[{"link_reach": "authenticated", "link_role": "editor"}],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
(
|
||||
[{"link_reach": "restricted", "link_role": "reader"}],
|
||||
{
|
||||
"restricted": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[{"link_reach": "restricted", "link_role": "editor"}],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
# Multiple ancestors with different roles
|
||||
(
|
||||
[
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "editor"},
|
||||
],
|
||||
{"public": ["editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "authenticated", "link_role": "editor"},
|
||||
],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "reader"},
|
||||
{"link_reach": "restricted", "link_role": "editor"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
# Multiple ancestors with different reaches
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "reader"},
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
# Multiple ancestors with mixed reaches and roles
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "editor"},
|
||||
{"link_reach": "public", "link_role": "reader"},
|
||||
],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
{"link_reach": "public", "link_role": "editor"},
|
||||
],
|
||||
{"public": ["editor"]},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "editor"},
|
||||
{"link_reach": "authenticated", "link_role": "reader"},
|
||||
],
|
||||
{
|
||||
"restricted": ["editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"public": ["reader", "editor"],
|
||||
},
|
||||
),
|
||||
(
|
||||
[
|
||||
{"link_reach": "restricted", "link_role": "reader"},
|
||||
{"link_reach": "authenticated", "link_role": "editor"},
|
||||
],
|
||||
{"authenticated": ["editor"], "public": ["reader", "editor"]},
|
||||
),
|
||||
# No ancestors (edge case)
|
||||
(
|
||||
[],
|
||||
None,
|
||||
"reader",
|
||||
{
|
||||
"public": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"restricted": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
),
|
||||
(
|
||||
None,
|
||||
None,
|
||||
{
|
||||
"public": ["reader", "editor"],
|
||||
"authenticated": ["reader", "editor"],
|
||||
"restricted": None,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_models_documents_get_select_options(ancestors_links, select_options):
|
||||
def test_models_documents_get_select_options(reach, role, select_options):
|
||||
"""Validate that the "get_select_options" method operates as expected."""
|
||||
assert models.LinkReachChoices.get_select_options(ancestors_links) == select_options
|
||||
assert models.LinkReachChoices.get_select_options(reach, role) == select_options
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_no_highest_readable():
|
||||
"""Test the compute_ancestors_links method."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
assert document.compute_ancestors_links(user=AnonymousUser()) == []
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_highest_readable(
|
||||
def test_models_documents_compute_ancestors_links_paths_mapping_single(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Test the compute_ancestors_links method."""
|
||||
"""Test the compute_ancestors_links_paths_mapping method on a single document."""
|
||||
document = factories.DocumentFactory(link_reach="public")
|
||||
with django_assert_num_queries(1):
|
||||
assert document.compute_ancestors_links_paths_mapping() == {
|
||||
document.path: [{"link_reach": "public", "link_role": document.link_role}]
|
||||
}
|
||||
|
||||
|
||||
def test_models_documents_compute_ancestors_links_paths_mapping_structure(
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Test the compute_ancestors_links_paths_mapping method on a tree of documents."""
|
||||
user = factories.UserFactory()
|
||||
other_user = factories.UserFactory()
|
||||
root = factories.DocumentFactory(
|
||||
link_reach="restricted", link_role="reader", users=[user]
|
||||
)
|
||||
|
||||
factories.DocumentFactory(
|
||||
parent=root, link_reach="public", link_role="reader", users=[user]
|
||||
)
|
||||
child2 = factories.DocumentFactory(
|
||||
root = factories.DocumentFactory(link_reach="restricted", users=[user])
|
||||
document = factories.DocumentFactory(
|
||||
parent=root,
|
||||
link_reach="authenticated",
|
||||
link_role="editor",
|
||||
users=[user, other_user],
|
||||
)
|
||||
child3 = factories.DocumentFactory(
|
||||
parent=child2,
|
||||
sibling = factories.DocumentFactory(parent=root, link_reach="public", users=[user])
|
||||
child = factories.DocumentFactory(
|
||||
parent=document,
|
||||
link_reach="authenticated",
|
||||
link_role="reader",
|
||||
users=[user, other_user],
|
||||
)
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
assert child3.compute_ancestors_links(user=user) == [
|
||||
{"link_reach": root.link_reach, "link_role": root.link_role},
|
||||
{"link_reach": child2.link_reach, "link_role": child2.link_role},
|
||||
]
|
||||
# Child
|
||||
with django_assert_num_queries(1):
|
||||
assert child.compute_ancestors_links_paths_mapping() == {
|
||||
root.path: [{"link_reach": "restricted", "link_role": root.link_role}],
|
||||
document.path: [
|
||||
{"link_reach": "restricted", "link_role": root.link_role},
|
||||
{"link_reach": document.link_reach, "link_role": document.link_role},
|
||||
],
|
||||
child.path: [
|
||||
{"link_reach": "restricted", "link_role": root.link_role},
|
||||
{"link_reach": document.link_reach, "link_role": document.link_role},
|
||||
{"link_reach": child.link_reach, "link_role": child.link_role},
|
||||
],
|
||||
}
|
||||
|
||||
with django_assert_num_queries(2):
|
||||
assert child3.compute_ancestors_links(user=other_user) == [
|
||||
{"link_reach": child2.link_reach, "link_role": child2.link_role},
|
||||
]
|
||||
# Sibling
|
||||
with django_assert_num_queries(1):
|
||||
assert sibling.compute_ancestors_links_paths_mapping() == {
|
||||
root.path: [{"link_reach": "restricted", "link_role": root.link_role}],
|
||||
sibling.path: [
|
||||
{"link_reach": "restricted", "link_role": root.link_role},
|
||||
{"link_reach": sibling.link_reach, "link_role": sibling.link_role},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Test converter services."""
|
||||
"""Test y-provider services."""
|
||||
|
||||
from base64 import b64decode
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from core.services.converter_services import (
|
||||
InvalidResponseError,
|
||||
MissingContentError,
|
||||
ServiceUnavailableError,
|
||||
ValidationError,
|
||||
YdocConverter,
|
||||
@@ -18,18 +17,18 @@ def test_auth_header(settings):
|
||||
"""Test authentication header generation."""
|
||||
settings.Y_PROVIDER_API_KEY = "test-key"
|
||||
converter = YdocConverter()
|
||||
assert converter.auth_header == "test-key"
|
||||
assert converter.auth_header == "Bearer test-key"
|
||||
|
||||
|
||||
def test_convert_markdown_empty_text():
|
||||
def test_convert_empty_text():
|
||||
"""Should raise ValidationError when text is empty."""
|
||||
converter = YdocConverter()
|
||||
with pytest.raises(ValidationError, match="Input text cannot be empty"):
|
||||
converter.convert_markdown("")
|
||||
converter.convert("")
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_convert_markdown_service_unavailable(mock_post):
|
||||
def test_convert_service_unavailable(mock_post):
|
||||
"""Should raise ServiceUnavailableError when service is unavailable."""
|
||||
converter = YdocConverter()
|
||||
|
||||
@@ -39,11 +38,11 @@ def test_convert_markdown_service_unavailable(mock_post):
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to conversion service",
|
||||
):
|
||||
converter.convert_markdown("test text")
|
||||
converter.convert("test text")
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_convert_markdown_http_error(mock_post):
|
||||
def test_convert_http_error(mock_post):
|
||||
"""Should raise ServiceUnavailableError when HTTP error occurs."""
|
||||
converter = YdocConverter()
|
||||
|
||||
@@ -55,46 +54,11 @@ def test_convert_markdown_http_error(mock_post):
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to conversion service",
|
||||
):
|
||||
converter.convert_markdown("test text")
|
||||
converter.convert("test text")
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_convert_markdown_invalid_json_response(mock_post):
|
||||
"""Should raise InvalidResponseError when response is not valid JSON."""
|
||||
converter = YdocConverter()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.side_effect = ValueError("Invalid JSON")
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with pytest.raises(
|
||||
InvalidResponseError,
|
||||
match="Could not parse conversion service response",
|
||||
):
|
||||
converter.convert_markdown("test text")
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_convert_markdown_missing_content_field(mock_post, settings):
|
||||
"""Should raise MissingContentError when response is missing required field."""
|
||||
|
||||
settings.CONVERSION_API_CONTENT_FIELD = "expected_field"
|
||||
|
||||
converter = YdocConverter()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"wrong_field": "content"}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
with pytest.raises(
|
||||
MissingContentError,
|
||||
match="Response missing required field: expected_field",
|
||||
):
|
||||
converter.convert_markdown("test text")
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_convert_markdown_full_integration(mock_post, settings):
|
||||
def test_convert_full_integration(mock_post, settings):
|
||||
"""Test full integration with all settings."""
|
||||
|
||||
settings.Y_PROVIDER_API_BASE_URL = "http://test.com/"
|
||||
@@ -105,20 +69,22 @@ def test_convert_markdown_full_integration(mock_post, settings):
|
||||
|
||||
converter = YdocConverter()
|
||||
|
||||
expected_content = {"converted": "content"}
|
||||
expected_content = b"converted content"
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"content": expected_content}
|
||||
mock_response.content = expected_content
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = converter.convert_markdown("test markdown")
|
||||
result = converter.convert("test markdown")
|
||||
|
||||
assert b64decode(result) == expected_content
|
||||
|
||||
assert result == expected_content
|
||||
mock_post.assert_called_once_with(
|
||||
"http://test.com/conversion-endpoint/",
|
||||
json={"content": "test markdown"},
|
||||
data="test markdown",
|
||||
headers={
|
||||
"Authorization": "test-key",
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer test-key",
|
||||
"Content-Type": "text/markdown",
|
||||
"Accept": "application/vnd.yjs.doc",
|
||||
},
|
||||
timeout=5,
|
||||
verify=False,
|
||||
@@ -126,7 +92,42 @@ def test_convert_markdown_full_integration(mock_post, settings):
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_convert_markdown_timeout(mock_post):
|
||||
def test_convert_full_integration_with_specific_headers(mock_post, settings):
|
||||
"""Test successful conversion with specific content type and accept headers."""
|
||||
settings.Y_PROVIDER_API_BASE_URL = "http://test.com/"
|
||||
settings.Y_PROVIDER_API_KEY = "test-key"
|
||||
settings.CONVERSION_API_ENDPOINT = "conversion-endpoint"
|
||||
settings.CONVERSION_API_TIMEOUT = 5
|
||||
settings.CONVERSION_API_SECURE = False
|
||||
|
||||
converter = YdocConverter()
|
||||
|
||||
expected_response = "# Test Document\n\nThis is test content."
|
||||
mock_response = MagicMock()
|
||||
mock_response.text = expected_response
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = converter.convert(
|
||||
b"test_content", "application/vnd.yjs.doc", "text/markdown"
|
||||
)
|
||||
|
||||
assert result == expected_response
|
||||
mock_post.assert_called_once_with(
|
||||
"http://test.com/conversion-endpoint/",
|
||||
data=b"test_content",
|
||||
headers={
|
||||
"Authorization": "Bearer test-key",
|
||||
"Content-Type": "application/vnd.yjs.doc",
|
||||
"Accept": "text/markdown",
|
||||
},
|
||||
timeout=5,
|
||||
verify=False,
|
||||
)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_convert_timeout(mock_post):
|
||||
"""Should raise ServiceUnavailableError when request times out."""
|
||||
converter = YdocConverter()
|
||||
|
||||
@@ -136,12 +137,12 @@ def test_convert_markdown_timeout(mock_post):
|
||||
ServiceUnavailableError,
|
||||
match="Failed to connect to conversion service",
|
||||
):
|
||||
converter.convert_markdown("test text")
|
||||
converter.convert("test text")
|
||||
|
||||
|
||||
def test_convert_markdown_none_input():
|
||||
def test_convert_none_input():
|
||||
"""Should raise ValidationError when input is None."""
|
||||
converter = YdocConverter()
|
||||
|
||||
with pytest.raises(ValidationError, match="Input text cannot be empty"):
|
||||
converter.convert_markdown(None)
|
||||
converter.convert(None)
|
||||
|
||||
@@ -16,6 +16,7 @@ from socket import gethostbyname, gethostname
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import dj_database_url
|
||||
import sentry_sdk
|
||||
from configurations import Configuration, values
|
||||
from csp.constants import NONE
|
||||
@@ -78,7 +79,9 @@ class Base(Configuration):
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"default": dj_database_url.config()
|
||||
if os.environ.get("DATABASE_URL")
|
||||
else {
|
||||
"ENGINE": values.Value(
|
||||
"django.db.backends.postgresql",
|
||||
environ_name="DB_ENGINE",
|
||||
@@ -291,6 +294,7 @@ class Base(Configuration):
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"core.middleware.ForceSessionMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"dockerflow.django.middleware.DockerflowMiddleware",
|
||||
"csp.middleware.CSPMiddleware",
|
||||
@@ -480,6 +484,7 @@ class Base(Configuration):
|
||||
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
|
||||
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
|
||||
)
|
||||
SESSION_COOKIE_NAME = "docs_sessionid"
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_CREATE_USER = values.BooleanValue(
|
||||
@@ -639,7 +644,7 @@ class Base(Configuration):
|
||||
|
||||
# Conversion endpoint
|
||||
CONVERSION_API_ENDPOINT = values.Value(
|
||||
default="convert-markdown",
|
||||
default="convert",
|
||||
environ_name="CONVERSION_API_ENDPOINT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
@@ -659,6 +664,12 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
NO_WEBSOCKET_CACHE_TIMEOUT = values.Value(
|
||||
default=120,
|
||||
environ_name="NO_WEBSOCKET_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
# to Sentry and don't want to log to console.
|
||||
@@ -853,15 +864,9 @@ class Development(Base):
|
||||
CSRF_TRUSTED_ORIGINS = ["http://localhost:8072", "http://localhost:3000"]
|
||||
DEBUG = True
|
||||
|
||||
SESSION_COOKIE_NAME = "impress_sessionid"
|
||||
|
||||
USE_SWAGGER = True
|
||||
SESSION_CACHE_ALIAS = "session"
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
|
||||
},
|
||||
"session": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": values.Value(
|
||||
"redis://redis:6379/2",
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Breton\n"
|
||||
"Language: br_FR\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Me eo an aozer"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Sinedoù"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
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:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
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:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "Korf"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr "Doare korf"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Stumm"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "eilenn {title}"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Lenner"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Embanner"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Merour"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Perc'henn"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Strishaet"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Anavezet"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Publik"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Bugel kentañ"
|
||||
@@ -81,11 +115,11 @@ msgstr "Bugel diwezhañ"
|
||||
|
||||
#: build/lib/core/enums.py:38 core/enums.py:38
|
||||
msgid "First sibling"
|
||||
msgstr ""
|
||||
msgstr "Breur pe c'hoar kentañ"
|
||||
|
||||
#: build/lib/core/enums.py:39 core/enums.py:39
|
||||
msgid "Last sibling"
|
||||
msgstr ""
|
||||
msgstr "Liamm diwezhañ"
|
||||
|
||||
#: build/lib/core/enums.py:40 core/enums.py:40
|
||||
msgid "Left"
|
||||
@@ -95,296 +129,293 @@ msgstr "Kleiz"
|
||||
msgid "Right"
|
||||
msgstr "Dehoù"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "Lenner"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Merour"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "Perc'henn"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Publik"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
msgstr "alc'hwez kentañ evit an enrollañ evel UIID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "krouet d'ar/al"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
msgstr "deiziad hag eurvezh krouidigezh an enrolladenn"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "hizivaet d'ar/al"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
msgstr "deiziad hag eurvezh m'eo bet hizivaet an enrolladenn"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr ""
|
||||
msgstr "N'hon eus kavet implijer ebet gant an isstrollad-mañ met ar postel a zo liammet ouzh un implijer enrollet."
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
msgstr "Ebarzhit un isstrollad mat. An talvoud-mañ a c'hall enderc'hel lizhiri, sifroù hag arouezioù @/./+/-/_/: hepken."
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
msgstr "isstrollad"
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr ""
|
||||
msgstr "Rekis. 255 arouezenn pe nebeutoc'h. Lizhiri, sifroù hag arouezioù @/./+/-/_/: hepken."
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "anv klok"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "anv berr"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
msgstr "postel identelezh"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
msgstr "postel ar merour"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "yezh"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
msgstr "Ar yezh en deus c'hoant da welet an implijer an etrefas enni."
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
msgstr "Ar gwerzhid-eur en deus c'hoant da welet an implijer an eur drezañ."
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "trevnad"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
msgstr "statud ar skipailh"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
msgstr "oberiant"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "implijer"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
msgstr "bomm"
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
msgstr "Restr"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
msgstr "Restroù"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
msgstr "Restr hep titl"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
|
||||
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
msgstr "Roud liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
msgstr "Roudoù liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
msgstr "Restr muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
msgstr "Restroù muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
msgstr "Liamm restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
msgstr "Liammoù restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr "Goulenn tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Goulennoù tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
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:1262 core/models.py:1262
|
||||
#, 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:1266 core/models.py:1266
|
||||
#, 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:1272 core/models.py:1272
|
||||
#, 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:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
msgstr "deskrivadur"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
msgstr "kod"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "publik"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "Patrom"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "Patromoù"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
msgstr "Liamm patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
msgstr "Liammoù patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
msgstr "An implijer-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
msgstr "postel"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
msgstr "Pedadenn d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
msgstr "Pedadennoù d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
msgstr "Logo ar postel"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Digeriñ"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
msgstr " Kinniget gant %(brandname)s "
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Ersteller bin ich"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favorit"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "Inhalt"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr "Typ"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "Kopie von {title}"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Lesen"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Bearbeiten"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Besitzer"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Beschränkt"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Authentifiziert"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Öffentlich"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Erstes Unterelement"
|
||||
@@ -95,295 +129,292 @@ msgstr "Links"
|
||||
msgid "Right"
|
||||
msgstr "Rechts"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "Lesen"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr "Bearbeiten"
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "Besitzer"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr "Beschränkt"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr "Authentifiziert"
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primärer Schlüssel für den Datensatz als UUID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "Erstellt"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Datum und Uhrzeit, an dem ein Datensatz erstellt wurde"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "Aktualisiert"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr "Wir konnten keinen Benutzer mit diesem Abo finden, aber die E-Mail-Adresse ist bereits einem registrierten Benutzer zugeordnet."
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr "unter"
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "Name"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "Kurzbezeichnung"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr "Admin E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "Status des Teammitgliedes"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "aktiviert"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr "Auszug"
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr "Dokument"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, 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:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
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:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
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:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
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:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr "Code"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo-E-Mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr " Docs, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Erstellt von %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
@@ -95,295 +129,292 @@ msgstr ""
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Yo soy el creador"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "¡Un nuevo documento se ha creado por ti!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
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:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "Cuerpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr "Tipo de Cuerpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia de {title}"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Lector"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Administrador"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Propietario"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Restringido"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Autentificado"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Público"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Primer nodo"
|
||||
@@ -95,295 +129,292 @@ msgstr "Izquierda"
|
||||
msgid "Right"
|
||||
msgstr "Derecha"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "Lector"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Administrador"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "Propietario"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr "Restringido"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr "Autentificado"
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Público"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clave primaria para el registro como UUID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "creado el"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "fecha y hora en la que se creó un registro"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "actualizado el"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "fecha y hora en la que un registro fue actualizado por última vez"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr "No se ha podido encontrar un usuario con este sub (UUID), pero el correo electrónico ya está asociado con un usuario."
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr "Introduzca un sub (UUID) válido. Este valor solo puede contener letras, números y los siguientes caracteres @/./+/-/_/:"
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr "sub (UUID)"
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "Requerido. 255 caracteres o menos. Letras, números y los siguientes caracteres @/./+/-/_/: solamente."
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "nombre completo"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "nombre abreviado"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr "correo electrónico de identidad"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr "correo electrónico del administrador"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "idioma"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "El idioma en el que el usuario desea ver la interfaz."
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "dispositivo"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si el usuario es un dispositivo o un usuario real."
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "rol en el equipo"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si el usuario puede iniciar sesión en esta página web de administración."
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "activo"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Si este usuario debe ser considerado como activo. Deseleccionar en lugar de eliminar cuentas."
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "usuario"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr "resumen"
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento sin título"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "¡{name} ha compartido un documento contigo!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, 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:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Traza del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Trazas del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
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:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
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:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Este usuario ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Este equipo ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
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:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
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:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "¡{name} desea acceder a un documento!"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr "código"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "público"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
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:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "Plantillas"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relación plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relaciones plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Este usuario ya forma parte de la plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Este equipo ya se encuentra en esta plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
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/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo de correo electrónico"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Abrir"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr "Docs, su nueva herramienta esencial para organizar, compartir y colaborar en sus documentos como equipo."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Presentado por %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Je suis l'auteur"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favoris"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nouveau document a été créé pour vous !"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
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:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "Corps"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr "Type de corps"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copie de {title}"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Lecteur"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Éditeur"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Restreint"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Authentifié"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Public"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Premier enfant"
|
||||
@@ -95,295 +129,292 @@ msgstr "Gauche"
|
||||
msgid "Right"
|
||||
msgstr "Droite"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "Lecteur"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr "Éditeur"
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr "Restreint"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr "Authentifié"
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Public"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr "identifiant/id"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clé primaire pour l'enregistrement en tant que UUID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "créé le"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date et heure de création de l'enregistrement"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr "Nous n'avons pas pu trouver un utilisateur avec ce sous-groupe mais l'e-mail est déjà associé à un utilisateur enregistré."
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr "Saisissez un sous-groupe valide. Cette valeur ne peut contenir que des lettres, des chiffres et les caractères @/./+/-/_/: uniquement."
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr "sous-groupe"
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_/: uniquement."
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr "adresse e-mail d'identité"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr "adresse e-mail de l'administrateur"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "statut d'équipe"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr "extrait"
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr "Document"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, 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:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Trace du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Traces du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
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:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
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:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Cet utilisateur est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Cette équipe est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
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:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
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:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} souhaiterait accéder au document suivant !"
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, 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:1272 core/models.py:1272
|
||||
#, 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:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "public"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
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:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "Modèles"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relation modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relations modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Cet utilisateur est déjà dans ce modèle."
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Cette équipe est déjà modèle."
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
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/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo de l'e-mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Ouvrir"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr " Docs, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Proposé par %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Il creatore sono io"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Preferiti"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nuovo documento è stato creato a tuo nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sei ora proprietario di un nuovo documento:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "Corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia di {title}"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Lettore"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Amministratore"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Proprietario"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Limitato"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Autenticato"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Pubblico"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
@@ -95,295 +129,292 @@ msgstr "Sinistra"
|
||||
msgid "Right"
|
||||
msgstr "Destra"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "Lettore"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Amministratore"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "Proprietario"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr "Limitato"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr "Autenticato"
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Pubblico"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr "Id"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "chiave primaria per il record come UUID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "creato il"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "data e ora in cui è stato creato un record"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "aggiornato il"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "data e ora in cui l’ultimo record è stato aggiornato"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "Richiesto. 255 caratteri o meno. Solo lettere, numeri e @/./+/-/_/: caratteri."
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "nome completo"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "nome"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr "indirizzo email di identità"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr "Indirizzo email dell'amministratore"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "lingua"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "dispositivo"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Se l'utente è un dispositivo o un utente reale."
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "stato del personale"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Indica se l'utente può accedere a questo sito amministratore."
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "attivo"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Indica se questo utente deve essere trattato come attivo. Deseleziona invece di eliminare gli account."
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "utente"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr "Documento"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento senza titolo"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ha condiviso un documento con te!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, 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:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Questo utente è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Questo team è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "pubblico"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Indica se questo modello è pubblico per chiunque."
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "Modello"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "Modelli"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Questo utente è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Questo team è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Questa email è già associata a un utente registrato."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo e-mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Apri"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Ik ben Eigenaar"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriete"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Een nieuw document was gecreëerd voor u!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "U heeft eigenaarschap van een nieuw document:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "Text"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr "Text type"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Formaat"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "kopie van {title}"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Lezer"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Bewerker"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Eigenaar"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Niet toegestaan"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Geauthenticeerd"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Publiek"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Eerste node"
|
||||
@@ -95,295 +129,292 @@ msgstr "Links"
|
||||
msgid "Right"
|
||||
msgstr "Rechts"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "Lezer"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr "Bewerker"
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "Eigenaar"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr "Niet toegestaan"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr "Geauthenticeerd"
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Publiek"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primaire sleutel voor dossier als UUID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "gemaakt op"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum en tijd wanneer dossier was gecreëerd"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "Laatst gewijzigd op"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "datum en tijd waarop dossier laatst was gewijzigd"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr "Wij konden geen gebruiker vinden met deze id, maar de email is al geassocieerd met een geregistreerde gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ".Geef een valide id. De waarde mag alleen letters, nummers en @/./.+/-/_: karakters bevatten."
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "Verplicht. 255 karakters of minder. Alleen letters, nummers en @/./+/-/_/: karakters zijn toegestaan."
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "gebruikersnaam"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr "identiteit email adres"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr "admin email adres"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "taal"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "De taal waarin de gebruiker de interface wilt zien."
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "De tijdzone waarin de gebruiker de tijden wilt zien."
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "apparaat"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "beheerder status"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Of de gebruiker kan inloggen in het admin gedeelte."
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "actief"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr "uittreksel"
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr "Document"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, 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:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, 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:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
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:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
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:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr "De gebruiker is al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Het team is al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
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:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr "omschrijving"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "publiek"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
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:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "Template"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "Templates"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr "Template/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr "Template/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr "De gebruiker bestaat al in dit template."
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Het team bestaat al in dit template."
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "email adres"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo email"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Open"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr " Docs, jouw nieuwe essentiële tool voor het organiseren, delen en collaboreren van documenten als team. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Geleverd door %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -19,57 +19,91 @@ msgstr ""
|
||||
|
||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
msgstr "Informações Pessoais"
|
||||
|
||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
||||
#: core/admin.py:138
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
msgstr "Permissões"
|
||||
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
msgstr "Datas importantes"
|
||||
|
||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
||||
msgid "Tree structure"
|
||||
msgstr ""
|
||||
msgstr "Estrutura de árvore"
|
||||
|
||||
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
msgstr "Título"
|
||||
|
||||
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
|
||||
msgid "Creator is me"
|
||||
msgstr ""
|
||||
msgstr "Eu sou o criador"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
msgstr "Um novo documento foi criado em seu nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
msgstr "A propriedade de um novo documento foi concedida a você:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
msgstr "Corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
msgstr "Tipo de corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
msgstr "cópia de {title}"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Leitor"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Editor"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Administrador"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Dono"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Restrito"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Autenticado"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Público"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
@@ -95,295 +129,292 @@ msgstr ""
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Language: sl_SI\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Ustvaril sem jaz"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Priljubljena"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
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:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
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:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "Telo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr "Vrsta telesa"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Oblika"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "Bralec"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "Urednik"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Skrbnik"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "Lastnik"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "Omejeno"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "Preverjeno"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Javno"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "Prvi otrok"
|
||||
@@ -95,295 +129,292 @@ msgstr "Levo"
|
||||
msgid "Right"
|
||||
msgstr "Desno"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "Bralec"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr "Urednik"
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Skrbnik"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "Lastnik"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr "Omejeno"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr "Preverjeno"
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Javno"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primarni ključ za zapis kot UUID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "ustvarjen na"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum in čas, ko je bil zapis ustvarjen"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "posodobljeno dne"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "datum in čas, ko je bil zapis nazadnje posodobljen"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr "Nismo mogli najti uporabnika s tem sub, vendar je e-poštni naslov že povezan z registriranim uporabnikom."
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr "Vnesite veljavno sub. Ta vrednost lahko vsebuje samo črke, številke in znake @/./+/-/_/:."
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "Obvezno. 255 znakov ali manj. Samo črke, številke in znaki @/./+/-/_/: ."
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "polno ime"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "kratko ime"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr "elektronski naslov identitete"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr "elektronski naslov skrbnika"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "jezik"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "naprava"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "kadrovski status"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "aktivni"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Ali je treba tega uporabnika obravnavati kot aktivnega. Namesto brisanja računov počistite to izbiro."
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr "odlomek"
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr "Dokument"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr "Dokument brez naslova"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} je delil dokument z vami!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, 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:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} je delil dokument z vami: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/sled povezave uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Sledi povezav dokumenta/uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
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:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Ta uporabnik je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ta ekipa je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
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:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr "koda"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "javno"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ali je ta predloga javna za uporabo."
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "Predloga"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "Predloge"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr "Odnos predloga/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr "Odnosi med predlogo in uporabnikom"
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Ta uporabnik je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ta ekipa je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
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/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "E-pošta z logotipom"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Odpri"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr " Dokumenti, vaše novo bistveno orodje za organiziranje, skupno rabo in skupinsko sodelovanje pri dokumentih. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " Pod okriljem %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Language: sv_SE\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "Skaparen är jag"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriter"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ett nytt dokument skapades åt dig!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
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:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "Administratör"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "Publik"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
@@ -95,295 +129,292 @@ msgstr ""
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "Administratör"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "Publik"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
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/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logotyp e-post"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öppna"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr ""
|
||||
@@ -95,295 +129,292 @@ msgstr ""
|
||||
msgid "Right"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-22 12:09+0000\n"
|
||||
"PO-Revision-Date: 2025-05-22 14:16\n"
|
||||
"POT-Creation-Date: 2025-07-24 20:42+0000\n"
|
||||
"PO-Revision-Date: 2025-07-31 12:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -43,34 +43,68 @@ msgid "Creator is me"
|
||||
msgstr "创建者是我"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "收藏"
|
||||
|
||||
#: build/lib/core/api/serializers.py:446 core/api/serializers.py:446
|
||||
#: build/lib/core/api/serializers.py:467 core/api/serializers.py:467
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "已为您创建了一份新文档!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:450 core/api/serializers.py:450
|
||||
#: build/lib/core/api/serializers.py:471 core/api/serializers.py:471
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "您已被授予新文档的所有权:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
|
||||
#: build/lib/core/api/serializers.py:608 core/api/serializers.py:608
|
||||
msgid "Body"
|
||||
msgstr "正文"
|
||||
|
||||
#: build/lib/core/api/serializers.py:589 core/api/serializers.py:589
|
||||
#: build/lib/core/api/serializers.py:611 core/api/serializers.py:611
|
||||
msgid "Body type"
|
||||
msgstr "正文类型"
|
||||
|
||||
#: build/lib/core/api/serializers.py:595 core/api/serializers.py:595
|
||||
#: build/lib/core/api/serializers.py:617 core/api/serializers.py:617
|
||||
msgid "Format"
|
||||
msgstr "格式"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:967 core/api/viewsets.py:967
|
||||
#: build/lib/core/api/viewsets.py:942 core/api/viewsets.py:942
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "{title} 的副本"
|
||||
|
||||
#: build/lib/core/choices.py:35 build/lib/core/choices.py:42 core/choices.py:35
|
||||
#: core/choices.py:42
|
||||
msgid "Reader"
|
||||
msgstr "阅读者"
|
||||
|
||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||
#: core/choices.py:43
|
||||
msgid "Editor"
|
||||
msgstr "编辑者"
|
||||
|
||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||
msgid "Administrator"
|
||||
msgstr "超级管理员"
|
||||
|
||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||
msgid "Owner"
|
||||
msgstr "所有者"
|
||||
|
||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||
msgid "Restricted"
|
||||
msgstr "受限的"
|
||||
|
||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||
msgid "Authenticated"
|
||||
msgstr "已验证"
|
||||
|
||||
#: build/lib/core/choices.py:62 core/choices.py:62
|
||||
msgid "Public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/enums.py:36 core/enums.py:36
|
||||
msgid "First child"
|
||||
msgstr "第一个子项"
|
||||
@@ -95,295 +129,292 @@ msgstr "左"
|
||||
msgid "Right"
|
||||
msgstr "右"
|
||||
|
||||
#: build/lib/core/models.py:56 build/lib/core/models.py:63 core/models.py:56
|
||||
#: core/models.py:63
|
||||
msgid "Reader"
|
||||
msgstr "阅读者"
|
||||
|
||||
#: build/lib/core/models.py:57 build/lib/core/models.py:64 core/models.py:57
|
||||
#: core/models.py:64
|
||||
msgid "Editor"
|
||||
msgstr "编辑者"
|
||||
|
||||
#: build/lib/core/models.py:65 core/models.py:65
|
||||
msgid "Administrator"
|
||||
msgstr "超级管理员"
|
||||
|
||||
#: build/lib/core/models.py:66 core/models.py:66
|
||||
msgid "Owner"
|
||||
msgstr "所有者"
|
||||
|
||||
#: build/lib/core/models.py:77 core/models.py:77
|
||||
msgid "Restricted"
|
||||
msgstr "受限的"
|
||||
|
||||
#: build/lib/core/models.py:81 core/models.py:81
|
||||
msgid "Authenticated"
|
||||
msgstr "已验证"
|
||||
|
||||
#: build/lib/core/models.py:83 core/models.py:83
|
||||
msgid "Public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
#: build/lib/core/models.py:79 core/models.py:79
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
#: build/lib/core/models.py:80 core/models.py:80
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "记录的主密钥为 UUID"
|
||||
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "created on"
|
||||
msgstr "创建时间"
|
||||
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
#: build/lib/core/models.py:87 core/models.py:87
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "记录的创建日期和时间"
|
||||
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
#: build/lib/core/models.py:92 core/models.py:92
|
||||
msgid "updated on"
|
||||
msgstr "更新时间"
|
||||
|
||||
#: build/lib/core/models.py:168 core/models.py:168
|
||||
#: build/lib/core/models.py:93 core/models.py:93
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "记录的最后更新时间"
|
||||
|
||||
#: build/lib/core/models.py:204 core/models.py:204
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr "未找到具有该 sub 的用户,但该邮箱已关联到一个注册用户。"
|
||||
|
||||
#: build/lib/core/models.py:217 core/models.py:217
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr "请输入有效的 sub。该值只能包含字母、数字及 @/./+/-/_/: 字符。"
|
||||
|
||||
#: build/lib/core/models.py:223 core/models.py:223
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: build/lib/core/models.py:225 core/models.py:225
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr "必填。最多 255 个字符,仅允许字母、数字及 @/./+/-/_/: 字符。"
|
||||
|
||||
#: build/lib/core/models.py:234 core/models.py:234
|
||||
#: build/lib/core/models.py:159 core/models.py:159
|
||||
msgid "full name"
|
||||
msgstr "全名"
|
||||
|
||||
#: build/lib/core/models.py:235 core/models.py:235
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "short name"
|
||||
msgstr "简称"
|
||||
|
||||
#: build/lib/core/models.py:237 core/models.py:237
|
||||
#: build/lib/core/models.py:162 core/models.py:162
|
||||
msgid "identity email address"
|
||||
msgstr "身份电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:242 core/models.py:242
|
||||
#: build/lib/core/models.py:167 core/models.py:167
|
||||
msgid "admin email address"
|
||||
msgstr "管理员电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:249 core/models.py:249
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "language"
|
||||
msgstr "语言"
|
||||
|
||||
#: build/lib/core/models.py:250 core/models.py:250
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "用户希望看到的界面语言。"
|
||||
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
#: build/lib/core/models.py:183 core/models.py:183
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "用户查看时间希望的时区。"
|
||||
|
||||
#: build/lib/core/models.py:261 core/models.py:261
|
||||
#: build/lib/core/models.py:186 core/models.py:186
|
||||
msgid "device"
|
||||
msgstr "设备"
|
||||
|
||||
#: build/lib/core/models.py:263 core/models.py:263
|
||||
#: build/lib/core/models.py:188 core/models.py:188
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "用户是设备还是真实用户。"
|
||||
|
||||
#: build/lib/core/models.py:266 core/models.py:266
|
||||
#: build/lib/core/models.py:191 core/models.py:191
|
||||
msgid "staff status"
|
||||
msgstr "员工状态"
|
||||
|
||||
#: build/lib/core/models.py:268 core/models.py:268
|
||||
#: build/lib/core/models.py:193 core/models.py:193
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "用户是否可以登录该管理员站点。"
|
||||
|
||||
#: build/lib/core/models.py:271 core/models.py:271
|
||||
#: build/lib/core/models.py:196 core/models.py:196
|
||||
msgid "active"
|
||||
msgstr "激活"
|
||||
|
||||
#: build/lib/core/models.py:274 core/models.py:274
|
||||
#: build/lib/core/models.py:199 core/models.py:199
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
|
||||
|
||||
#: build/lib/core/models.py:286 core/models.py:286
|
||||
#: build/lib/core/models.py:211 core/models.py:211
|
||||
msgid "user"
|
||||
msgstr "用户"
|
||||
|
||||
#: build/lib/core/models.py:287 core/models.py:287
|
||||
#: build/lib/core/models.py:212 core/models.py:212
|
||||
msgid "users"
|
||||
msgstr "个用户"
|
||||
|
||||
#: build/lib/core/models.py:470 build/lib/core/models.py:1155
|
||||
#: core/models.py:470 core/models.py:1155
|
||||
#: build/lib/core/models.py:368 build/lib/core/models.py:1283
|
||||
#: core/models.py:368 core/models.py:1283
|
||||
msgid "title"
|
||||
msgstr "标题"
|
||||
|
||||
#: build/lib/core/models.py:471 core/models.py:471
|
||||
#: build/lib/core/models.py:369 core/models.py:369
|
||||
msgid "excerpt"
|
||||
msgstr "摘要"
|
||||
|
||||
#: build/lib/core/models.py:519 core/models.py:519
|
||||
#: build/lib/core/models.py:418 core/models.py:418
|
||||
msgid "Document"
|
||||
msgstr "文档"
|
||||
|
||||
#: build/lib/core/models.py:520 core/models.py:520
|
||||
#: build/lib/core/models.py:419 core/models.py:419
|
||||
msgid "Documents"
|
||||
msgstr "个文档"
|
||||
|
||||
#: build/lib/core/models.py:532 build/lib/core/models.py:873 core/models.py:532
|
||||
#: core/models.py:873
|
||||
#: build/lib/core/models.py:431 build/lib/core/models.py:821 core/models.py:431
|
||||
#: core/models.py:821
|
||||
msgid "Untitled Document"
|
||||
msgstr "未命名文档"
|
||||
|
||||
#: build/lib/core/models.py:908 core/models.py:908
|
||||
#: build/lib/core/models.py:856 core/models.py:856
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} 与您共享了一个文档!"
|
||||
|
||||
#: build/lib/core/models.py:912 core/models.py:912
|
||||
#: build/lib/core/models.py:860 core/models.py:860
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:918 core/models.py:918
|
||||
#: build/lib/core/models.py:866 core/models.py:866
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} 与您共享了一个文档:{title}"
|
||||
|
||||
#: build/lib/core/models.py:1016 core/models.py:1016
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:1017 core/models.py:1017
|
||||
#: build/lib/core/models.py:967 core/models.py:967
|
||||
msgid "Document/user link traces"
|
||||
msgstr "个文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:973 core/models.py:973
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "此文档/用户的链接跟踪已存在。"
|
||||
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorite"
|
||||
msgstr "文档收藏"
|
||||
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
#: build/lib/core/models.py:997 core/models.py:997
|
||||
msgid "Document favorites"
|
||||
msgstr "文档收藏夹"
|
||||
|
||||
#: build/lib/core/models.py:1053 core/models.py:1053
|
||||
#: build/lib/core/models.py:1003 core/models.py:1003
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "该文档已被同一用户的收藏关系实例关联。"
|
||||
|
||||
#: build/lib/core/models.py:1075 core/models.py:1075
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relation"
|
||||
msgstr "文档/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1076 core/models.py:1076
|
||||
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||
msgid "Document/user relations"
|
||||
msgstr "文档/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1082 core/models.py:1082
|
||||
#: build/lib/core/models.py:1032 core/models.py:1032
|
||||
msgid "This user is already in this document."
|
||||
msgstr "该用户已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1088 core/models.py:1088
|
||||
#: build/lib/core/models.py:1038 core/models.py:1038
|
||||
msgid "This team is already in this document."
|
||||
msgstr "该团队已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1094 build/lib/core/models.py:1242
|
||||
#: core/models.py:1094 core/models.py:1242
|
||||
#: build/lib/core/models.py:1044 build/lib/core/models.py:1369
|
||||
#: core/models.py:1044 core/models.py:1369
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "必须设置用户或团队之一,不能同时设置两者。"
|
||||
|
||||
#: build/lib/core/models.py:1156 core/models.py:1156
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1197 core/models.py:1197
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1262 core/models.py:1262
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1266 core/models.py:1266
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1272 core/models.py:1272
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "description"
|
||||
msgstr "说明"
|
||||
|
||||
#: build/lib/core/models.py:1157 core/models.py:1157
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "code"
|
||||
msgstr "代码"
|
||||
|
||||
#: build/lib/core/models.py:1158 core/models.py:1158
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1160 core/models.py:1160
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
msgid "public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:1162 core/models.py:1162
|
||||
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "该模板是否公开供任何人使用。"
|
||||
|
||||
#: build/lib/core/models.py:1168 core/models.py:1168
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Template"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1169 core/models.py:1169
|
||||
#: build/lib/core/models.py:1297 core/models.py:1297
|
||||
msgid "Templates"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1223 core/models.py:1223
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relation"
|
||||
msgstr "模板/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1224 core/models.py:1224
|
||||
#: build/lib/core/models.py:1351 core/models.py:1351
|
||||
msgid "Template/user relations"
|
||||
msgstr "模板/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1230 core/models.py:1230
|
||||
#: build/lib/core/models.py:1357 core/models.py:1357
|
||||
msgid "This user is already in this template."
|
||||
msgstr "该用户已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1236 core/models.py:1236
|
||||
#: build/lib/core/models.py:1363 core/models.py:1363
|
||||
msgid "This team is already in this template."
|
||||
msgstr "该团队已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||
#: build/lib/core/models.py:1440 core/models.py:1440
|
||||
msgid "email address"
|
||||
msgstr "电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitation"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||
#: build/lib/core/models.py:1460 core/models.py:1460
|
||||
msgid "Document invitations"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1299 core/models.py:1299
|
||||
#: build/lib/core/models.py:1480 core/models.py:1480
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "此电子邮件已经与现有注册用户关联。"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "徽标邮件"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/text/template.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr " Docs——您的全新必备工具,帮助团队组织、共享和协作处理文档。 "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#: core/templates/mail/html/template.html:233
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr " 由 %(brandname)s 倾力打造。 "
|
||||
|
||||
+15
-14
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "3.3.0"
|
||||
version = "3.5.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -26,20 +26,21 @@ readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"beautifulsoup4==4.13.4",
|
||||
"boto3==1.38.46",
|
||||
"boto3==1.39.4",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.5.3",
|
||||
"dj-database-url==2.3.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-csp==4.0",
|
||||
"django-filter==25.1",
|
||||
"django-lasuite[all]==0.0.10",
|
||||
"django-lasuite[all]==0.0.11",
|
||||
"django-parler==2.3",
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.3",
|
||||
"django==5.2.4",
|
||||
"django-treebeard==4.7.1",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
@@ -52,9 +53,9 @@ dependencies = [
|
||||
"markdown==3.8.2",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.93.0",
|
||||
"openai==1.95.0",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"pycrdt==0.12.23",
|
||||
"pycrdt==0.12.25",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
"redis<6.0.0",
|
||||
@@ -64,19 +65,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/numerique-gouv/impress/issues/new"
|
||||
"Changelog" = "https://github.com/numerique-gouv/impress/blob/main/CHANGELOG.md"
|
||||
"Homepage" = "https://github.com/numerique-gouv/impress"
|
||||
"Repository" = "https://github.com/numerique-gouv/impress"
|
||||
"Bug Tracker" = "https://github.com/suitenumerique/docs/issues/new"
|
||||
"Changelog" = "https://github.com/suitenumerique/docs/blob/main/CHANGELOG.md"
|
||||
"Homepage" = "https://github.com/suitenumerique/docs"
|
||||
"Repository" = "https://github.com/suitenumerique/docs"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"django-test-migrations==1.5.0",
|
||||
"drf-spectacular-sidecar==2025.6.1",
|
||||
"drf-spectacular-sidecar==2025.7.1",
|
||||
"freezegun==1.5.2",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.3.0",
|
||||
"ipython==9.4.0",
|
||||
"pyfakefs==5.9.1",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.7",
|
||||
@@ -84,9 +85,9 @@ dev = [
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==8.4.1",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.7.0",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.12.1",
|
||||
"ruff==0.12.2",
|
||||
"types-requests==2.32.4.20250611",
|
||||
]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(
|
||||
page.locator('header').first().locator('h2').getByText('Docs'),
|
||||
page.locator('header').first().locator('h1').getByText('Docs'),
|
||||
).toBeVisible();
|
||||
await page.goto('unknown-page404');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FullConfig, FullProject, chromium, expect } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
import { keyCloakSignIn } from './utils-common';
|
||||
|
||||
const saveStorageState = async (
|
||||
browserConfig: FullProject<unknown, unknown>,
|
||||
@@ -23,7 +23,7 @@ const saveStorageState = async (
|
||||
page.locator('header').first().getByRole('button', {
|
||||
name: 'Logout',
|
||||
}),
|
||||
).toBeVisible();
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await page.context().storageState({
|
||||
path: storageState as string,
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { CONFIG, createDoc, overrideConfig } from './common';
|
||||
import { CONFIG, createDoc, overrideConfig } from './utils-common';
|
||||
|
||||
test.describe('Config', () => {
|
||||
test('it checks that sentry is trying to init from config endpoint', async ({
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
keyCloakSignIn,
|
||||
randomName,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
} from './utils-common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -14,21 +14,65 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.describe('Doc Create', () => {
|
||||
test('it creates a doc', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, 'My new doc', browserName, 1);
|
||||
const [docTitle] = await createDoc(page, 'my-new-doc', browserName, 1);
|
||||
|
||||
await page.waitForFunction(
|
||||
() => document.title.match(/My new doc - Docs/),
|
||||
() => document.title.match(/my-new-doc - Docs/),
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
await expect(docsGrid.getByText(docTitle)).toBeVisible();
|
||||
});
|
||||
|
||||
test('it creates a sub doc from slash menu editor', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [title] = await createDoc(page, 'my-new-slash-doc', browserName, 1);
|
||||
|
||||
await verifyDocName(page, title);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page
|
||||
.getByText('New sub-doc', {
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(input).toHaveText('');
|
||||
await expect(
|
||||
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it creates a sub doc from interlinking dropdown', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [title] = await createDoc(page, 'my-new-slash-doc', browserName, 1);
|
||||
|
||||
await verifyDocName(page, title);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Link a doc').first().click();
|
||||
await page
|
||||
.locator('.quick-search-container')
|
||||
.getByText('New sub-doc')
|
||||
.click();
|
||||
|
||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(input).toHaveText('');
|
||||
await expect(
|
||||
page.locator('.c__tree-view--row-content').getByText('Untitled document'),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Create: Not logged', () => {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { chromium, expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
|
||||
import {
|
||||
CONFIG,
|
||||
addNewMember,
|
||||
createDoc,
|
||||
goToGridDoc,
|
||||
mockedDocument,
|
||||
overrideConfig,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
} from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -173,6 +173,7 @@ test.describe('Doc Editor', () => {
|
||||
await expect(editor.getByText('Hello World Doc 2')).toBeHidden();
|
||||
await expect(editor.getByText('Hello World Doc 1')).toBeVisible();
|
||||
|
||||
await page.goto('/');
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'New doc',
|
||||
@@ -522,52 +523,151 @@ test.describe('Doc Editor', () => {
|
||||
|
||||
test('it checks block editing when not connected to collab server', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
...CONFIG,
|
||||
COLLABORATION_WS_URL: 'ws://localhost:5555/collaboration/ws/',
|
||||
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
test.slow();
|
||||
|
||||
/**
|
||||
* The good port is 4444, but we want to simulate a not connected
|
||||
* collaborative server.
|
||||
* So we use a port that is not used by the collaborative server.
|
||||
* The server will not be able to connect to the collaborative server.
|
||||
*/
|
||||
await overrideConfig(page, {
|
||||
COLLABORATION_WS_URL: 'ws://localhost:5555/collaboration/ws/',
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'New doc',
|
||||
})
|
||||
.click();
|
||||
const [parentTitle] = await createDoc(
|
||||
page,
|
||||
'editing-blocking',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
const card = page.getByLabel('It is the card information');
|
||||
await expect(
|
||||
card.getByText('Your network do not allow you to edit'),
|
||||
card.getByText('Others are editing. Your network prevent changes.'),
|
||||
).toBeHidden();
|
||||
const editor = page.locator('.ProseMirror');
|
||||
|
||||
await expect(editor).toHaveAttribute('contenteditable', 'true');
|
||||
|
||||
let responseCanEditPromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/can-edit/`) && response.status() === 200,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await addNewMember(page, 0, 'Editor', 'impress');
|
||||
await page.getByLabel('Visibility', { exact: true }).click();
|
||||
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'Public',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document visibility has been updated.'),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Visibility mode').click();
|
||||
await page.getByRole('menuitem', { name: 'Editing' }).click();
|
||||
|
||||
// Close the modal
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
|
||||
const urlParentDoc = page.url();
|
||||
|
||||
const { name: childTitle } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'editing-blocking - child',
|
||||
);
|
||||
|
||||
let responseCanEdit = await responseCanEditPromise;
|
||||
expect(responseCanEdit.ok()).toBeTruthy();
|
||||
let jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
|
||||
expect(jsonCanEdit.can_edit).toBeTruthy();
|
||||
|
||||
const urlChildDoc = page.url();
|
||||
|
||||
/**
|
||||
* 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 otherContext.newPage();
|
||||
|
||||
const webSocketPromise = otherPage.waitForEvent(
|
||||
'websocket',
|
||||
(webSocket) => {
|
||||
return webSocket
|
||||
.url()
|
||||
.includes('ws://localhost:4444/collaboration/ws/?room=');
|
||||
},
|
||||
);
|
||||
|
||||
await otherPage.goto(urlChildDoc);
|
||||
|
||||
const webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toContain(
|
||||
'ws://localhost:4444/collaboration/ws/?room=',
|
||||
);
|
||||
|
||||
await verifyDocName(otherPage, childTitle);
|
||||
|
||||
await page.reload();
|
||||
|
||||
responseCanEditPromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/can-edit/`) && response.status() === 200,
|
||||
);
|
||||
|
||||
responseCanEdit = await responseCanEditPromise;
|
||||
expect(responseCanEdit.ok()).toBeTruthy();
|
||||
|
||||
jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
|
||||
expect(jsonCanEdit.can_edit).toBeFalsy();
|
||||
|
||||
await expect(
|
||||
card.getByText('Your network do not allow you to edit'),
|
||||
card.getByText('Others are editing. Your network prevent changes.'),
|
||||
).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(editor).toHaveAttribute('contenteditable', 'false');
|
||||
|
||||
await page.goto(urlParentDoc);
|
||||
|
||||
await verifyDocName(page, parentTitle);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await page.getByLabel('Visibility mode').click();
|
||||
await page.getByRole('menuitem', { name: 'Reading' }).click();
|
||||
|
||||
// Close the modal
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
|
||||
await page.goto(urlChildDoc);
|
||||
|
||||
await expect(editor).toHaveAttribute('contenteditable', 'true');
|
||||
|
||||
await expect(
|
||||
card.getByText('Others are editing. Your network prevent changes.'),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks if callout custom block', async ({ page, browserName }) => {
|
||||
@@ -606,4 +706,75 @@ test.describe('Doc Editor', () => {
|
||||
'pink',
|
||||
);
|
||||
});
|
||||
|
||||
test('it checks interlink feature', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const { name: docChild1 } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'doc-interlink-child-1',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild1);
|
||||
|
||||
const { name: docChild2 } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'doc-interlink-child-2',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild2);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
const input = page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
);
|
||||
const searchContainer = page.locator('.quick-search-container');
|
||||
|
||||
await input.fill('doc-interlink');
|
||||
|
||||
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
|
||||
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
||||
await expect(searchContainer.getByText(docChild2)).toBeVisible();
|
||||
|
||||
await input.pressSequentially('-child');
|
||||
|
||||
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
||||
await expect(searchContainer.getByText(docChild2)).toBeVisible();
|
||||
await expect(searchContainer.getByText(randomDoc)).toBeHidden();
|
||||
|
||||
// use keydown to select the second result
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
const interlink = page.getByRole('link', {
|
||||
name: 'child-2',
|
||||
});
|
||||
|
||||
await expect(interlink).toBeVisible();
|
||||
await interlink.click();
|
||||
|
||||
await verifyDocName(page, docChild2);
|
||||
});
|
||||
|
||||
test('it checks interlink shortcut @', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const editor = page.locator('.bn-block-outer').last();
|
||||
await editor.click();
|
||||
await page.keyboard.press('@');
|
||||
|
||||
await expect(
|
||||
page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,14 @@ import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import pdf from 'pdf-parse';
|
||||
|
||||
import { createDoc, verifyDocName } from './common';
|
||||
import {
|
||||
TestLanguage,
|
||||
createDoc,
|
||||
randomName,
|
||||
verifyDocName,
|
||||
waitForLanguageSwitch,
|
||||
} from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -72,6 +79,7 @@ test.describe('Doc Export', () => {
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -82,6 +90,7 @@ test.describe('Doc Export', () => {
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -120,6 +129,7 @@ test.describe('Doc Export', () => {
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -129,6 +139,7 @@ test.describe('Doc Export', () => {
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -139,6 +150,7 @@ test.describe('Doc Export', () => {
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -312,12 +324,14 @@ test.describe('Doc Export', () => {
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -328,6 +342,7 @@ test.describe('Doc Export', () => {
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
@@ -338,4 +353,204 @@ test.describe('Doc Export', () => {
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
expect(pdfData.text).toContain('Hello World');
|
||||
});
|
||||
|
||||
test('it exports the doc with multi columns', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-multi-columns',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
|
||||
await page.getByText('Three Columns', { exact: true }).click();
|
||||
|
||||
await page.locator('.bn-block-column').first().fill('Column 1');
|
||||
await page.locator('.bn-block-column').nth(1).fill('Column 2');
|
||||
await page.locator('.bn-block-column').last().fill('Column 3');
|
||||
|
||||
expect(await page.locator('.bn-block-column').count()).toBe(3);
|
||||
await expect(
|
||||
page.locator('.bn-block-column[data-node-type="column"]').first(),
|
||||
).toHaveText('Column 1');
|
||||
await expect(
|
||||
page.locator('.bn-block-column[data-node-type="column"]').nth(1),
|
||||
).toHaveText('Column 2');
|
||||
await expect(
|
||||
page.locator('.bn-block-column[data-node-type="column"]').last(),
|
||||
).toHaveText('Column 3');
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
expect(pdfData.text).toContain('Column 1');
|
||||
expect(pdfData.text).toContain('Column 2');
|
||||
expect(pdfData.text).toContain('Column 3');
|
||||
});
|
||||
|
||||
test('it injects the correct language attribute into PDF export', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await waitForLanguageSwitch(page, TestLanguage.French);
|
||||
|
||||
// Wait for the page to be ready after language switch
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const randomDocFrench = randomName(
|
||||
'doc-language-export-french',
|
||||
browserName,
|
||||
1,
|
||||
)[0];
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Nouveau doc',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.waitForURL('**/docs/**', {
|
||||
timeout: 10000,
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
const input = page.getByLabel('doc title input');
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toHaveText('');
|
||||
await input.click();
|
||||
await input.fill(randomDocFrench);
|
||||
await input.blur();
|
||||
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
await editor.click();
|
||||
await editor.fill('Contenu de test pour export en français');
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDocFrench}.pdf`);
|
||||
});
|
||||
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Télécharger',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfString = pdfBuffer.toString('latin1');
|
||||
|
||||
expect(pdfString).toContain('/Lang (fr)');
|
||||
});
|
||||
|
||||
test('it exports the doc with interlinking', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'export-interlinking',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
const { name: docChild } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'export-interlink-child',
|
||||
);
|
||||
|
||||
await verifyDocName(page, docChild);
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
await page
|
||||
.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
)
|
||||
.fill('interlink-child');
|
||||
|
||||
await page
|
||||
.locator('.quick-search-container')
|
||||
.getByText('interlink-child')
|
||||
.click();
|
||||
|
||||
const interlink = page.getByRole('link', {
|
||||
name: 'interlink-child',
|
||||
});
|
||||
|
||||
await expect(interlink).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${docChild}.pdf`);
|
||||
});
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
void page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
|
||||
expect(pdfData.text).toContain('interlink-child'); // This is the pdf text
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, mockedListDocs } from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.describe('Doc grid dnd', () => {
|
||||
test('it creates a doc', async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
const header = page.locator('header').first();
|
||||
await createDoc(page, 'Draggable doc', browserName, 1);
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
await createDoc(page, 'Droppable doc', browserName, 1);
|
||||
await header.locator('h1').getByText('Docs').click();
|
||||
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().endsWith('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const responseJson = await response.json();
|
||||
|
||||
const items = responseJson.results;
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
const draggableElement = page.getByTestId(`draggable-doc-${items[1].id}`);
|
||||
const dropZone = page.getByTestId(`droppable-doc-${items[0].id}`);
|
||||
await expect(draggableElement).toBeVisible();
|
||||
await expect(dropZone).toBeVisible();
|
||||
|
||||
// Obtenir les positions des éléments
|
||||
const draggableBoundingBox = await draggableElement.boundingBox();
|
||||
const dropZoneBoundingBox = await dropZone.boundingBox();
|
||||
|
||||
expect(draggableBoundingBox).toBeDefined();
|
||||
expect(dropZoneBoundingBox).toBeDefined();
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!draggableBoundingBox || !dropZoneBoundingBox) {
|
||||
throw new Error('Impossible de déterminer la position des éléments');
|
||||
}
|
||||
|
||||
await page.mouse.move(
|
||||
draggableBoundingBox.x + draggableBoundingBox.width / 2,
|
||||
draggableBoundingBox.y + draggableBoundingBox.height / 2,
|
||||
);
|
||||
await page.mouse.down();
|
||||
|
||||
// Déplacer vers la zone cible
|
||||
await page.mouse.move(
|
||||
dropZoneBoundingBox.x + dropZoneBoundingBox.width / 2,
|
||||
dropZoneBoundingBox.y + dropZoneBoundingBox.height / 2,
|
||||
{ steps: 10 }, // Make the movement smoother
|
||||
);
|
||||
|
||||
const dragOverlay = page.getByTestId('drag-doc-overlay');
|
||||
|
||||
await expect(dragOverlay).toBeVisible();
|
||||
await expect(dragOverlay).toHaveText(items[1].title as string);
|
||||
await page.mouse.up();
|
||||
|
||||
await expect(dragOverlay).toBeHidden();
|
||||
});
|
||||
|
||||
test("it checks can't drop when we have not the minimum role", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockedListDocs(page, data);
|
||||
await page.goto('/');
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag');
|
||||
|
||||
const noDropAndNoDrag = page.getByTestId(
|
||||
'droppable-doc-no-drop-and-no-drag',
|
||||
);
|
||||
|
||||
await expect(canDropAndDrag).toBeVisible();
|
||||
|
||||
await expect(noDropAndNoDrag).toBeVisible();
|
||||
|
||||
const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox();
|
||||
|
||||
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
|
||||
throw new Error('Impossible de déterminer la position des éléments');
|
||||
}
|
||||
|
||||
await page.mouse.move(
|
||||
canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2,
|
||||
canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2,
|
||||
);
|
||||
|
||||
await page.mouse.down();
|
||||
|
||||
await page.mouse.move(
|
||||
noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2,
|
||||
noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2,
|
||||
{ steps: 10 },
|
||||
);
|
||||
|
||||
const dragOverlay = page.getByTestId('drag-doc-overlay');
|
||||
|
||||
await expect(dragOverlay).toBeVisible();
|
||||
await expect(dragOverlay).toHaveText(
|
||||
'You must be at least the editor of the target document',
|
||||
);
|
||||
|
||||
await page.mouse.up();
|
||||
});
|
||||
|
||||
test("it checks can't drag when we have not the minimum role", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockedListDocs(page, data);
|
||||
await page.goto('/');
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(docsGrid).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
const canDropAndDrag = page.getByTestId('droppable-doc-can-drop-and-drag');
|
||||
|
||||
const noDropAndNoDrag = page.getByTestId(
|
||||
'droppable-doc-no-drop-and-no-drag',
|
||||
);
|
||||
|
||||
await expect(canDropAndDrag).toBeVisible();
|
||||
|
||||
await expect(noDropAndNoDrag).toBeVisible();
|
||||
|
||||
const canDropAndDragBoundigBox = await canDropAndDrag.boundingBox();
|
||||
|
||||
const noDropAndNoDragBoundigBox = await noDropAndNoDrag.boundingBox();
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!canDropAndDragBoundigBox || !noDropAndNoDragBoundigBox) {
|
||||
throw new Error('Impossible de déterminer la position des éléments');
|
||||
}
|
||||
|
||||
await page.mouse.move(
|
||||
noDropAndNoDragBoundigBox.x + noDropAndNoDragBoundigBox.width / 2,
|
||||
noDropAndNoDragBoundigBox.y + noDropAndNoDragBoundigBox.height / 2,
|
||||
);
|
||||
|
||||
await page.mouse.down();
|
||||
|
||||
await page.mouse.move(
|
||||
canDropAndDragBoundigBox.x + canDropAndDragBoundigBox.width / 2,
|
||||
canDropAndDragBoundigBox.y + canDropAndDragBoundigBox.height / 2,
|
||||
{ steps: 10 },
|
||||
);
|
||||
|
||||
const dragOverlay = page.getByTestId('drag-doc-overlay');
|
||||
|
||||
await expect(dragOverlay).toBeVisible();
|
||||
await expect(dragOverlay).toHaveText(
|
||||
'You must be the owner to move the document',
|
||||
);
|
||||
|
||||
await page.mouse.up();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc grid dnd mobile', () => {
|
||||
test.use({ viewport: { width: 500, height: 1200 } });
|
||||
|
||||
test('DND is deactivated on mobile', async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const docsGrid = page.getByTestId('docs-grid');
|
||||
await expect(page.getByTestId('docs-grid')).toBeVisible();
|
||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
||||
|
||||
await expect(docsGrid.getByRole('row').first()).toBeVisible();
|
||||
await expect(docsGrid.locator('.--docs--grid-droppable')).toHaveCount(0);
|
||||
|
||||
await createDoc(page, 'Draggable doc mobile', browserName, 1, true);
|
||||
|
||||
await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'Draggable doc mobile child',
|
||||
true,
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'Open the header menu' })
|
||||
.getByText('menu')
|
||||
.click();
|
||||
|
||||
await expect(page.locator('.--docs-sub-page-item').first()).toHaveAttribute(
|
||||
'draggable',
|
||||
'false',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 'can-drop-and-drag',
|
||||
abilities: {
|
||||
accesses_manage: true,
|
||||
accesses_view: true,
|
||||
ai_transform: true,
|
||||
ai_translate: true,
|
||||
attachment_upload: true,
|
||||
children_list: true,
|
||||
children_create: true,
|
||||
collaboration_auth: true,
|
||||
descendants: true,
|
||||
destroy: true,
|
||||
favorite: true,
|
||||
link_configuration: true,
|
||||
invite_owner: true,
|
||||
move: true,
|
||||
partial_update: true,
|
||||
restore: true,
|
||||
retrieve: true,
|
||||
media_auth: true,
|
||||
link_select_options: {
|
||||
restricted: ['reader', 'editor'],
|
||||
authenticated: ['reader', 'editor'],
|
||||
public: ['reader', 'editor'],
|
||||
},
|
||||
tree: true,
|
||||
update: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
},
|
||||
created_at: '2025-03-14T14:45:22.527221Z',
|
||||
creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb',
|
||||
depth: 1,
|
||||
excerpt: null,
|
||||
is_favorite: false,
|
||||
link_role: 'reader',
|
||||
link_reach: 'restricted',
|
||||
nb_accesses_ancestors: 1,
|
||||
nb_accesses_direct: 1,
|
||||
numchild: 5,
|
||||
path: '000000o',
|
||||
title: 'Can drop and drag',
|
||||
updated_at: '2025-03-14T14:45:27.699542Z',
|
||||
user_roles: ['owner'],
|
||||
user_role: 'owner',
|
||||
},
|
||||
{
|
||||
id: 'can-only-drop',
|
||||
title: 'Can only drop',
|
||||
abilities: {
|
||||
accesses_manage: true,
|
||||
accesses_view: true,
|
||||
ai_transform: true,
|
||||
ai_translate: true,
|
||||
attachment_upload: true,
|
||||
children_list: true,
|
||||
children_create: true,
|
||||
collaboration_auth: true,
|
||||
descendants: true,
|
||||
destroy: true,
|
||||
favorite: true,
|
||||
link_configuration: true,
|
||||
invite_owner: true,
|
||||
move: true,
|
||||
partial_update: true,
|
||||
restore: true,
|
||||
retrieve: true,
|
||||
media_auth: true,
|
||||
link_select_options: {
|
||||
restricted: ['reader', 'editor'],
|
||||
authenticated: ['reader', 'editor'],
|
||||
public: ['reader', 'editor'],
|
||||
},
|
||||
tree: true,
|
||||
update: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
},
|
||||
created_at: '2025-03-14T14:45:22.527221Z',
|
||||
creator: 'bc6895e0-8f6d-4b00-827d-c143aa6b2ecb',
|
||||
depth: 1,
|
||||
excerpt: null,
|
||||
is_favorite: false,
|
||||
link_role: 'reader',
|
||||
link_reach: 'restricted',
|
||||
nb_accesses_ancestors: 1,
|
||||
nb_accesses_direct: 1,
|
||||
numchild: 5,
|
||||
path: '000000o',
|
||||
|
||||
updated_at: '2025-03-14T14:45:27.699542Z',
|
||||
user_roles: ['editor'],
|
||||
user_role: 'editor',
|
||||
},
|
||||
{
|
||||
id: 'no-drop-and-no-drag',
|
||||
abilities: {
|
||||
accesses_manage: false,
|
||||
accesses_view: true,
|
||||
ai_transform: false,
|
||||
ai_translate: false,
|
||||
attachment_upload: false,
|
||||
children_list: true,
|
||||
children_create: false,
|
||||
collaboration_auth: true,
|
||||
descendants: true,
|
||||
destroy: false,
|
||||
favorite: true,
|
||||
link_configuration: false,
|
||||
invite_owner: false,
|
||||
move: false,
|
||||
partial_update: false,
|
||||
restore: false,
|
||||
retrieve: true,
|
||||
media_auth: true,
|
||||
link_select_options: {
|
||||
restricted: ['reader', 'editor'],
|
||||
authenticated: ['reader', 'editor'],
|
||||
public: ['reader', 'editor'],
|
||||
},
|
||||
tree: true,
|
||||
update: false,
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
},
|
||||
created_at: '2025-03-14T14:44:16.032773Z',
|
||||
creator: '9264f420-f018-4bd6-96ae-4788f41af56d',
|
||||
depth: 1,
|
||||
excerpt: null,
|
||||
is_favorite: false,
|
||||
link_role: 'reader',
|
||||
link_reach: 'restricted',
|
||||
nb_accesses_ancestors: 14,
|
||||
nb_accesses_direct: 14,
|
||||
numchild: 0,
|
||||
path: '000000l',
|
||||
title: 'No drop and no drag',
|
||||
updated_at: '2025-03-14T14:44:16.032774Z',
|
||||
user_roles: ['reader'],
|
||||
user_role: 'reader',
|
||||
},
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, getGridRow } from './common';
|
||||
import { createDoc, getGridRow } from './utils-common';
|
||||
|
||||
type SmallDoc = {
|
||||
id: string;
|
||||
@@ -59,6 +59,7 @@ test.describe('Documents Grid mobile', () => {
|
||||
link_reach: 'public',
|
||||
created_at: '2024-10-07T13:02:41.085298Z',
|
||||
updated_at: '2024-10-07T13:30:21.829690Z',
|
||||
user_roles: ['owner'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -90,6 +91,22 @@ test.describe('Document grid item options', () => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('it checks the share modal', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, `check share modal`, browserName);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByText(docTitle)).toBeVisible();
|
||||
const row = await getGridRow(page, docTitle);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Share' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('dialog').getByText('Share the document'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it pins a document', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, `Favorite doc`, browserName);
|
||||
|
||||
@@ -102,7 +119,7 @@ test.describe('Document grid item options', () => {
|
||||
await page.getByText('push_pin').click();
|
||||
|
||||
// Check is pinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeVisible();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
|
||||
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
|
||||
|
||||
@@ -111,7 +128,7 @@ test.describe('Document grid item options', () => {
|
||||
await page.getByText('Unpin').click();
|
||||
|
||||
// Check is unpinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeHidden();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
|
||||
});
|
||||
|
||||
@@ -168,6 +185,8 @@ test.describe('Document grid item options', () => {
|
||||
},
|
||||
link_reach: 'restricted',
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
user_roles: ['editor'],
|
||||
user_role: 'editor',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -208,21 +227,23 @@ test.describe('Documents filters', () => {
|
||||
|
||||
// Initial state
|
||||
await expect(allDocs).toBeVisible();
|
||||
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(allDocs).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
await expect(myDocs).toBeVisible();
|
||||
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(myDocs).not.toHaveAttribute('aria-current');
|
||||
|
||||
await expect(sharedWithMe).toBeVisible();
|
||||
await expect(sharedWithMe).toHaveCSS(
|
||||
'background-color',
|
||||
'rgba(0, 0, 0, 0)',
|
||||
);
|
||||
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
|
||||
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
|
||||
|
||||
await allDocs.click();
|
||||
|
||||
await page.waitForURL('**/?target=all_docs');
|
||||
|
||||
let url = new URL(page.url());
|
||||
let target = url.searchParams.get('target');
|
||||
expect(target).toBe('all_docs');
|
||||
|
||||
@@ -4,60 +4,22 @@ import {
|
||||
createDoc,
|
||||
getGridRow,
|
||||
goToGridDoc,
|
||||
mockedAccesses,
|
||||
mockedDocument,
|
||||
mockedInvitations,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
} from './utils-common';
|
||||
import { mockedAccesses, mockedInvitations } from './utils-share';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Header', () => {
|
||||
test('it checks the element are correctly displayed', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
accesses: [
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super@owner.com',
|
||||
full_name: 'Super Owner',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'admin',
|
||||
user: {
|
||||
email: 'super@admin.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super2@owner.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
abilities: {
|
||||
destroy: true, // Means owner
|
||||
link_configuration: true,
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
accesses_manage: true,
|
||||
accesses_view: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
link_reach: 'public',
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
test('it checks the element are correctly displayed', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-update', browserName, 1);
|
||||
|
||||
const card = page.getByLabel(
|
||||
'It is the card information about the document.',
|
||||
@@ -66,6 +28,18 @@ test.describe('Doc Header', () => {
|
||||
const docTitle = card.getByRole('textbox', { name: 'doc title input' });
|
||||
await expect(docTitle).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
await page.getByLabel('Visibility', { exact: true }).click();
|
||||
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'Public',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
|
||||
await expect(card.getByText('Public document')).toBeVisible();
|
||||
|
||||
await expect(card.getByText('Owner ·')).toBeVisible();
|
||||
@@ -95,9 +69,7 @@ test.describe('Doc Header', () => {
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText(`Are you sure you want to delete this document ?`),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(`This document and any sub-`)).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -158,32 +130,31 @@ test.describe('Doc Header', () => {
|
||||
await expect(shareModal).toBeVisible();
|
||||
await expect(page.getByText('Share the document')).toBeVisible();
|
||||
|
||||
await expect(page.getByPlaceholder('Type a name or email')).toBeVisible();
|
||||
|
||||
const invitationCard = shareModal.getByLabel('List invitation card');
|
||||
await expect(invitationCard).toBeVisible();
|
||||
await expect(
|
||||
invitationCard.getByText('test@invitation.test').first(),
|
||||
).toBeVisible();
|
||||
await expect(invitationCard.getByLabel('doc-role-dropdown')).toBeVisible();
|
||||
const invitationRole = invitationCard.getByLabel('doc-role-dropdown');
|
||||
await expect(invitationRole).toBeVisible();
|
||||
|
||||
await invitationCard.getByRole('button', { name: 'more_horiz' }).click();
|
||||
await invitationRole.click();
|
||||
|
||||
await expect(page.getByLabel('Delete')).toBeEnabled();
|
||||
await invitationCard.click();
|
||||
await page.getByRole('menuitem', { name: 'Remove access' }).click();
|
||||
await expect(invitationCard).toBeHidden();
|
||||
|
||||
const memberCard = shareModal.getByLabel('List members card');
|
||||
const roles = memberCard.getByLabel('doc-role-dropdown');
|
||||
await expect(memberCard).toBeVisible();
|
||||
await expect(
|
||||
memberCard.getByText('test@accesses.test').first(),
|
||||
).toBeVisible();
|
||||
await expect(memberCard.getByLabel('doc-role-dropdown')).toBeVisible();
|
||||
await expect(
|
||||
memberCard.getByRole('button', { name: 'more_horiz' }),
|
||||
).toBeVisible();
|
||||
await memberCard.getByRole('button', { name: 'more_horiz' }).click();
|
||||
await expect(roles).toBeVisible();
|
||||
|
||||
await expect(page.getByLabel('Delete')).toBeEnabled();
|
||||
await roles.click();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Remove access' }),
|
||||
).toBeEnabled();
|
||||
});
|
||||
|
||||
test('it checks the options available if editor', async ({ page }) => {
|
||||
@@ -424,7 +395,7 @@ test.describe('Doc Header', () => {
|
||||
});
|
||||
|
||||
test('it pins a document', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, `Favorite doc`, browserName);
|
||||
const [docTitle] = await createDoc(page, `Pin doc`, browserName);
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
@@ -438,7 +409,7 @@ test.describe('Doc Header', () => {
|
||||
const row = await getGridRow(page, docTitle);
|
||||
|
||||
// Check is pinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeVisible();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeVisible();
|
||||
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeVisible();
|
||||
|
||||
@@ -453,9 +424,73 @@ test.describe('Doc Header', () => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check is unpinned
|
||||
await expect(row.getByLabel('Pin document icon')).toBeHidden();
|
||||
await expect(row.locator('[data-testid^="doc-pinned-"]')).toBeHidden();
|
||||
await expect(leftPanelFavorites.getByText(docTitle)).toBeHidden();
|
||||
});
|
||||
|
||||
test('it duplicates a document', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, `Duplicate doc`, browserName);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
await editor.fill('Hello Duplicated World');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
await expect(
|
||||
page.getByText('Document duplicated successfully!'),
|
||||
).toBeVisible();
|
||||
|
||||
const duplicateTitle = 'Copy of ' + docTitle;
|
||||
await verifyDocName(page, duplicateTitle);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const row = await getGridRow(page, duplicateTitle);
|
||||
|
||||
await expect(row.getByText(duplicateTitle)).toBeVisible();
|
||||
|
||||
await row.getByText(`more_horiz`).click();
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle;
|
||||
await page.getByText(duplicateDuplicateTitle).click();
|
||||
await expect(page.getByText('Hello Duplicated World')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it duplicates a child document', async ({ page, browserName }) => {
|
||||
await createDoc(page, `Duplicate doc`, browserName);
|
||||
|
||||
const { name: childTitle } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'Duplicate doc - child',
|
||||
);
|
||||
|
||||
const editor = page.locator('.ProseMirror');
|
||||
await editor.click();
|
||||
await editor.fill('Hello Duplicated World');
|
||||
|
||||
const duplicateTitle = 'Copy of ' + childTitle;
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
|
||||
const child = docTree
|
||||
.getByRole('treeitem')
|
||||
.locator('.--docs-sub-page-item')
|
||||
.filter({
|
||||
hasText: childTitle,
|
||||
});
|
||||
await child.hover();
|
||||
await child.getByText(`more_horiz`).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
|
||||
await verifyDocName(page, duplicateTitle);
|
||||
|
||||
await expect(
|
||||
page.getByTestId('doc-tree').getByText(duplicateTitle),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents Header mobile', () => {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import { updateShareLink } from './utils-share';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.describe('Inherited share accesses', () => {
|
||||
test('it checks inherited accesses', async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
const [parentTitle] = await createDoc(page, 'root-doc', browserName, 1);
|
||||
|
||||
// Wait for and intercept the POST request to create a new page
|
||||
await createRootSubPage(page, browserName, 'sub-page');
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await expect(
|
||||
page.getByText('People with access via the parent document'),
|
||||
).toBeVisible();
|
||||
|
||||
const user = page.getByTestId(
|
||||
`doc-share-member-row-user@${browserName}.test`,
|
||||
);
|
||||
await expect(user).toBeVisible();
|
||||
await expect(user.getByText('E2E Chromium')).toBeVisible();
|
||||
await expect(user.getByText('Owner')).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator('.--docs--doc-inherited-share-content')
|
||||
.getByRole('link')
|
||||
.click();
|
||||
|
||||
await verifyDocName(page, parentTitle);
|
||||
});
|
||||
|
||||
test('it checks if the link is inherited', async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
// Create root doc
|
||||
await createDoc(page, 'root-doc', browserName, 1);
|
||||
|
||||
// Update share link
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
await updateShareLink(page, 'Connected', 'Reading');
|
||||
await page.getByRole('button', { name: 'OK' }).click();
|
||||
|
||||
// Create sub page
|
||||
await createRootSubPage(page, browserName, 'sub-page');
|
||||
|
||||
// Verify share link is like the parent document
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
const docVisibilityCard = page.getByLabel('Doc visibility card');
|
||||
|
||||
await expect(docVisibilityCard.getByText('Connected')).toBeVisible();
|
||||
await expect(docVisibilityCard.getByText('Reading')).toBeVisible();
|
||||
|
||||
// Verify inherited link
|
||||
await docVisibilityCard.getByText('Connected').click();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Private' }),
|
||||
).toBeDisabled();
|
||||
|
||||
// Update child link
|
||||
await page.getByRole('menuitem', { name: 'Public' }).click();
|
||||
|
||||
await docVisibilityCard.getByText('Reading').click();
|
||||
await page.getByRole('menuitem', { name: 'Editing' }).click();
|
||||
|
||||
await expect(docVisibilityCard.getByText('Connected')).toBeHidden();
|
||||
await expect(docVisibilityCard.getByText('Reading')).toBeHidden();
|
||||
await expect(
|
||||
docVisibilityCard.getByText('Public', {
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(docVisibilityCard.getByText('Editing')).toBeVisible();
|
||||
await expect(
|
||||
docVisibilityCard.getByText(
|
||||
'The link sharing rules differ from the parent document',
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
// Restore inherited link
|
||||
await page.getByRole('button', { name: 'Restore' }).click();
|
||||
|
||||
await expect(docVisibilityCard.getByText('Connected')).toBeVisible();
|
||||
await expect(docVisibilityCard.getByText('Reading')).toBeVisible();
|
||||
await expect(docVisibilityCard.getByText('Public')).toBeHidden();
|
||||
await expect(docVisibilityCard.getByText('Editing')).toBeHidden();
|
||||
await expect(
|
||||
docVisibilityCard.getByText(
|
||||
'The link sharing rules differ from the parent document',
|
||||
),
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
keyCloakSignIn,
|
||||
randomName,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
} from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.describe('Document create member', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -243,9 +244,7 @@ test.describe('Document create member: Multiple login', () => {
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible();
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -270,9 +269,9 @@ test.describe('Document create member: Multiple login', () => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Docs Logo Docs' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
@@ -294,4 +293,59 @@ test.describe('Document create member: Multiple login', () => {
|
||||
await expect(page.getByText('Share with 2 users')).toBeVisible();
|
||||
await expect(page.getByText(`E2E ${otherBrowser}`)).toBeVisible();
|
||||
});
|
||||
|
||||
test('It cannot request member access on child doc on a 403 page', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
|
||||
const [docParent] = await createDoc(
|
||||
page,
|
||||
'Block Member access request on child doc - parent',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, docParent);
|
||||
|
||||
await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'Block Member access request on child doc - child',
|
||||
);
|
||||
|
||||
const urlDoc = page.url();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Logout',
|
||||
})
|
||||
.click();
|
||||
|
||||
const otherBrowser = BROWSERS.find((b) => b !== browserName);
|
||||
|
||||
await keyCloakSignIn(page, otherBrowser!);
|
||||
|
||||
await expect(page.getByTestId('header-logo-link')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await page.goto(urlDoc);
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
"You're currently viewing a sub-document. To gain access, please request permission from the main document.",
|
||||
),
|
||||
).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Request access' }),
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { addNewMember, createDoc, goToGridDoc, verifyDocName } from './common';
|
||||
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -8,47 +9,59 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.describe('Document list members', () => {
|
||||
test('it checks a big list of members', async ({ page }) => {
|
||||
await page.route(
|
||||
/.*\/documents\/.*\/accesses\/\?page=.*/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pageId = url.searchParams.get('page') ?? '1';
|
||||
|
||||
const accesses = {
|
||||
count: 40,
|
||||
next: +pageId < 2 ? 'http://anything/?page=2' : undefined,
|
||||
previous: null,
|
||||
results: Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`,
|
||||
user: {
|
||||
id: `fc092149-cafa-4ffa-a29d-e4b18af751-${pageId}-${i}`,
|
||||
email: `impress@impress.world-page-${pageId}-${i}`,
|
||||
full_name: `Impress World Page ${pageId}-${i}`,
|
||||
},
|
||||
team: '',
|
||||
role: 'editor',
|
||||
abilities: {
|
||||
destroy: false,
|
||||
partial_update: true,
|
||||
set_role_to: [],
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: accesses,
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const docTitle = await goToGridDoc(page);
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
// Get the current URL and extract the last part
|
||||
const currentUrl = page.url();
|
||||
|
||||
const currentDocId = (() => {
|
||||
// Remove trailing slash if present
|
||||
const cleanUrl = currentUrl.endsWith('/')
|
||||
? currentUrl.slice(0, -1)
|
||||
: currentUrl;
|
||||
|
||||
// Split by '/' and get the last part
|
||||
return cleanUrl.split('/').pop() || '';
|
||||
})();
|
||||
|
||||
await page.route('**/documents/**/accesses/', async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pageId = url.searchParams.get('page') ?? '1';
|
||||
|
||||
const accesses = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`,
|
||||
document: {
|
||||
id: currentDocId,
|
||||
name: `Doc ${pageId}-${i}`,
|
||||
path: `0000.${pageId}-${i}`,
|
||||
},
|
||||
user: {
|
||||
id: `fc092149-cafa-4ffa-a29d-e4b18af751-${pageId}-${i}`,
|
||||
email: `impress@impress.world-page-${pageId}-${i}`,
|
||||
full_name: `Impress World Page ${pageId}-${i}`,
|
||||
},
|
||||
team: '',
|
||||
role: 'editor',
|
||||
max_ancestors_role: null,
|
||||
max_role: 'editor',
|
||||
abilities: {
|
||||
destroy: false,
|
||||
partial_update: true,
|
||||
set_role_to: ['administrator', 'editor'],
|
||||
},
|
||||
}));
|
||||
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: accesses,
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const prefix = 'doc-share-member-row';
|
||||
@@ -56,11 +69,6 @@ test.describe('Document list members', () => {
|
||||
const loadMore = page.getByTestId('load-more-members');
|
||||
|
||||
await expect(elements).toHaveCount(20);
|
||||
await expect(page.getByText(`Impress World Page 1-16`)).toBeVisible();
|
||||
|
||||
await loadMore.click();
|
||||
await expect(elements).toHaveCount(40);
|
||||
await expect(page.getByText(`Impress World Page 2-15`)).toBeVisible();
|
||||
|
||||
await expect(loadMore).toBeHidden();
|
||||
});
|
||||
@@ -141,7 +149,11 @@ test.describe('Document list members', () => {
|
||||
`You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`,
|
||||
);
|
||||
await expect(soloOwner).toBeVisible();
|
||||
await list.click();
|
||||
|
||||
await list.click({
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true, // Force click to close the dropdown
|
||||
});
|
||||
const newUserEmail = await addNewMember(page, 0, 'Owner');
|
||||
const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`);
|
||||
const newUserRoles = newUser.getByLabel('doc-role-dropdown');
|
||||
@@ -150,10 +162,16 @@ test.describe('Document list members', () => {
|
||||
|
||||
await currentUserRole.click();
|
||||
await expect(soloOwner).toBeHidden();
|
||||
await list.click();
|
||||
await list.click({
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true, // Force click to close the dropdown
|
||||
});
|
||||
|
||||
await newUserRoles.click();
|
||||
await list.click();
|
||||
await list.click({
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true, // Force click to close the dropdown
|
||||
});
|
||||
|
||||
await currentUserRole.click();
|
||||
await page.getByLabel('Administrator').click();
|
||||
@@ -177,17 +195,14 @@ test.describe('Document list members', () => {
|
||||
|
||||
const emailMyself = `user@${browserName}.test`;
|
||||
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
|
||||
const mySelfMoreActions = mySelf.getByRole('button', {
|
||||
name: 'more_horiz',
|
||||
const mySelfRole = mySelf.getByRole('button', {
|
||||
name: 'doc-role-dropdown',
|
||||
});
|
||||
|
||||
const userOwnerEmail = await addNewMember(page, 0, 'Owner');
|
||||
const userOwner = list.getByTestId(
|
||||
`doc-share-member-row-${userOwnerEmail}`,
|
||||
);
|
||||
const userOwnerMoreActions = userOwner.getByRole('button', {
|
||||
name: 'more_horiz',
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).first().click();
|
||||
await page.getByRole('button', { name: 'Share' }).first().click();
|
||||
@@ -197,24 +212,20 @@ test.describe('Document list members', () => {
|
||||
const userReader = list.getByTestId(
|
||||
`doc-share-member-row-${userReaderEmail}`,
|
||||
);
|
||||
const userReaderMoreActions = userReader.getByRole('button', {
|
||||
name: 'more_horiz',
|
||||
const userReaderRole = userReader.getByRole('button', {
|
||||
name: 'doc-role-dropdown',
|
||||
});
|
||||
|
||||
await expect(mySelf).toBeVisible();
|
||||
await expect(userOwner).toBeVisible();
|
||||
await expect(userReader).toBeVisible();
|
||||
|
||||
await expect(userOwnerMoreActions).toBeVisible();
|
||||
await expect(userReaderMoreActions).toBeVisible();
|
||||
await expect(mySelfMoreActions).toBeVisible();
|
||||
|
||||
await userReaderMoreActions.click();
|
||||
await page.getByLabel('Delete').click();
|
||||
await userReaderRole.click();
|
||||
await page.getByRole('menuitem', { name: 'Remove access' }).click();
|
||||
await expect(userReader).toBeHidden();
|
||||
|
||||
await mySelfMoreActions.click();
|
||||
await page.getByLabel('Delete').click();
|
||||
await mySelfRole.click();
|
||||
await page.getByRole('menuitem', { name: 'Remove access' }).click();
|
||||
await expect(
|
||||
page.getByText('Insufficient access rights to view the document.'),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -8,25 +8,19 @@ import {
|
||||
keyCloakSignIn,
|
||||
mockedDocument,
|
||||
verifyDocName,
|
||||
} from './common';
|
||||
} from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.describe('Doc Routing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('Check the presence of the meta tag noindex', async ({ page }) => {
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'New doc',
|
||||
});
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
await buttonCreateHomepage.click();
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Share',
|
||||
}),
|
||||
).toBeVisible();
|
||||
test('Check the presence of the meta tag noindex', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'doc-routing-test', browserName, 1);
|
||||
const metaDescription = page.locator('meta[name="robots"]');
|
||||
await expect(metaDescription).toHaveAttribute('content', 'noindex');
|
||||
});
|
||||
@@ -60,16 +54,20 @@ test.describe('Doc Routing', () => {
|
||||
});
|
||||
|
||||
test('checks 401 on docs/[id] page', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, 'My new doc', browserName, 1);
|
||||
const [docTitle] = await createDoc(page, '401-doc-parent', browserName, 1);
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
await createRootSubPage(page, browserName, '401-doc-child');
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
const responsePromise = page.route(
|
||||
/.*\/link-configuration\/$|users\/me\/$/,
|
||||
/.*\/documents\/.*\/$|users\/me\/$/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
|
||||
if (
|
||||
request.method().includes('PUT') ||
|
||||
request.method().includes('PATCH') ||
|
||||
request.method().includes('GET')
|
||||
) {
|
||||
await route.fulfill({
|
||||
@@ -84,11 +82,7 @@ test.describe('Doc Routing', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
||||
await selectVisibility.click();
|
||||
await page.getByLabel('Connected').click();
|
||||
await page.getByRole('link', { name: '401-doc-parent' }).click();
|
||||
|
||||
await responsePromise;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, verifyDocName } from './common';
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -25,7 +26,7 @@ test.describe('Document search', () => {
|
||||
);
|
||||
await verifyDocName(page, doc2Title);
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'search' }).click();
|
||||
await page.getByTestId('search-docs-button').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('img', { name: 'No active search' }),
|
||||
@@ -94,4 +95,114 @@ test.describe('Document search', () => {
|
||||
page.getByLabel('Search modal').getByText('search'),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it check the presence of filters in search modal', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
// Doc grid filters are not visible
|
||||
const searchButton = page.getByTestId('search-docs-button');
|
||||
|
||||
const filters = page.getByTestId('doc-search-filters');
|
||||
|
||||
await searchButton.click();
|
||||
await expect(
|
||||
page.getByRole('combobox', { name: 'Quick search input' }),
|
||||
).toBeVisible();
|
||||
await expect(filters).toBeHidden();
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).click();
|
||||
|
||||
// Create a doc without children for the moment
|
||||
// and check that filters are not visible
|
||||
const [doc1Title] = await createDoc(page, 'My page search', browserName, 1);
|
||||
await verifyDocName(page, doc1Title);
|
||||
|
||||
await searchButton.click();
|
||||
await expect(
|
||||
page.getByRole('combobox', { name: 'Quick search input' }),
|
||||
).toBeVisible();
|
||||
await expect(filters).toBeHidden();
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).click();
|
||||
|
||||
// Create a sub page
|
||||
// and check that filters are visible
|
||||
await createRootSubPage(page, browserName, 'My sub page search');
|
||||
|
||||
await searchButton.click();
|
||||
|
||||
await expect(filters).toBeVisible();
|
||||
|
||||
await filters.click();
|
||||
await filters.getByRole('button', { name: 'Current doc' }).click();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'All docs' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('menuitem', { name: 'Current doc' }),
|
||||
).toBeVisible();
|
||||
await page.getByRole('menuitem', { name: 'All docs' }).click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('it searches sub pages', async ({ page, browserName }) => {
|
||||
// First doc
|
||||
const [firstDocTitle] = await createDoc(
|
||||
page,
|
||||
'My first sub page search',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, firstDocTitle);
|
||||
|
||||
// Create a new doc - for the moment without children
|
||||
const [secondDocTitle] = await createDoc(
|
||||
page,
|
||||
'My second sub page search',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
const searchButton = page.getByTestId('search-docs-button');
|
||||
|
||||
await searchButton.click();
|
||||
await page.getByRole('combobox', { name: 'Quick search input' }).click();
|
||||
await page
|
||||
.getByRole('combobox', { name: 'Quick search input' })
|
||||
.fill('sub page search');
|
||||
|
||||
// Expect to find the first doc
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(firstDocTitle),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(secondDocTitle),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'close' }).click();
|
||||
|
||||
// Create a sub page
|
||||
const { name: secondChildDocTitle } = await createRootSubPage(
|
||||
page,
|
||||
browserName,
|
||||
'second - Child doc',
|
||||
);
|
||||
await searchButton.click();
|
||||
await page
|
||||
.getByRole('combobox', { name: 'Quick search input' })
|
||||
.fill('second');
|
||||
|
||||
// Now there is a sub page - expect to have the focus on the current doc
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(secondDocTitle),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(secondChildDocTitle),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('presentation').getByLabel(firstDocTitle),
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, verifyDocName } from './common';
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user