Compare commits
135 Commits
feat/stati
...
feat/e2e-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0144044c55 | ||
|
|
a6da37e231 | ||
|
|
9aeedd1d03 | ||
|
|
f7d4e6810b | ||
|
|
b740ffa52c | ||
|
|
f555e36e98 | ||
|
|
de11ab508f | ||
|
|
dc2fe4905b | ||
|
|
2864669dde | ||
|
|
7dae3a3c02 | ||
|
|
bdf62e2172 | ||
|
|
29104dfe2d | ||
|
|
785c9b21cf | ||
|
|
3fee1f2081 | ||
|
|
5f9968d81e | ||
|
|
f7baf238e3 | ||
|
|
bab42efd08 | ||
|
|
175d80db16 | ||
|
|
f8b8390758 | ||
|
|
a1463e0a10 | ||
|
|
0b555eed9f | ||
|
|
1bf810d596 | ||
|
|
48e1370ba3 | ||
|
|
b13571c6df | ||
|
|
a2a63cd13e | ||
|
|
3ebb62d786 | ||
|
|
0caee61d86 | ||
|
|
10a319881d | ||
|
|
26620f3471 | ||
|
|
0d0e17c8d5 | ||
|
|
257de6d068 | ||
|
|
5a4c02a978 | ||
|
|
0090ccc981 | ||
|
|
d403878f8c | ||
|
|
191b046641 | ||
|
|
aeac49d760 | ||
|
|
b5dcbbb057 | ||
|
|
2e64298ff4 | ||
|
|
8dad9ea6c4 | ||
|
|
3ae8046ffc | ||
|
|
a4e3168682 | ||
|
|
c8955133a4 | ||
|
|
b069310bf0 | ||
|
|
1292c33a58 | ||
|
|
bf68a5ae40 | ||
|
|
8799b4aa2f | ||
|
|
d96abb1ccf | ||
|
|
dc12a99d4a | ||
|
|
82a0c1a770 | ||
|
|
a758254b60 | ||
|
|
6314cb3a18 | ||
|
|
3e410e3519 | ||
|
|
aba7959344 | ||
|
|
3d45c7c215 | ||
|
|
cdb26b480a | ||
|
|
23a0f2761f | ||
|
|
0d596e338c | ||
|
|
3ab01c98c8 | ||
|
|
6445c05e29 | ||
|
|
b9b25eb1f6 | ||
|
|
de157b4f52 | ||
|
|
e5581e52f7 | ||
|
|
b91840c819 | ||
|
|
a9b77fb9a7 | ||
|
|
66f83db0e5 | ||
|
|
f9ff578c6b | ||
|
|
1372438f8e | ||
|
|
c5d5d3dec4 | ||
|
|
ad16c0843c | ||
|
|
78a6307656 | ||
|
|
d7d468f51f | ||
|
|
eb71028f6b | ||
|
|
39c22b074d | ||
|
|
d5c3f248a5 | ||
|
|
91217b3c4f | ||
|
|
ab271bc90d | ||
|
|
82e1783317 | ||
|
|
aa2b9ed5f2 | ||
|
|
1c96d645ba | ||
|
|
2f010cf36d | ||
|
|
9d3c1eb9d5 | ||
|
|
08f3ceaf3f | ||
|
|
b1d033edc9 | ||
|
|
192fa76b54 | ||
|
|
b667200ebd | ||
|
|
294922f966 | ||
|
|
8b73aa3644 | ||
|
|
dd56a8abeb | ||
|
|
145c688830 | ||
|
|
950d215632 | ||
|
|
7d5cc4e84b | ||
|
|
3e5bcf96ea | ||
|
|
fe24c00178 | ||
|
|
aca334f81f | ||
|
|
2003e41c22 | ||
|
|
5ebdf4b4d4 | ||
|
|
35e771a1ce | ||
|
|
2b5a9e1af8 | ||
|
|
a833fdc7a1 | ||
|
|
b3cc2bf833 | ||
|
|
18feab10cb | ||
|
|
2777488d24 | ||
|
|
a11258f778 | ||
|
|
33647f124f | ||
|
|
e339cda5c6 | ||
|
|
4ce65c654f | ||
|
|
c048b2ae95 | ||
|
|
5908afb098 | ||
|
|
e2298a3658 | ||
|
|
278eb233e9 | ||
|
|
b056dbfad4 | ||
|
|
771ef2417f | ||
|
|
8d5262c2f2 | ||
|
|
1125f441dc | ||
|
|
16f2de4c75 | ||
|
|
f19fa93600 | ||
|
|
af3d90db3b | ||
|
|
127c90ca5f | ||
|
|
fa7cf7a594 | ||
|
|
6523165ea0 | ||
|
|
de4d11732f | ||
|
|
37138c1a23 | ||
|
|
2c1a9ff74f | ||
|
|
31389bcae2 | ||
|
|
f772801fd0 | ||
|
|
390a615f48 | ||
|
|
5bdf5d2210 | ||
|
|
ed336558ac | ||
|
|
4fbd588198 | ||
|
|
546f97c956 | ||
|
|
af01c6e466 | ||
|
|
8023720da3 | ||
|
|
91eba31735 | ||
|
|
45d6c1beef | ||
|
|
dc25f3f39c |
6
.github/workflows/helmfile-linter.yaml
vendored
@@ -21,10 +21,10 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
HELMFILE=src/helm/helmfile.yaml
|
HELMFILE=src/helm/helmfile.yaml.gotmpl
|
||||||
environments=$(awk 'BEGIN {in_env=0} /^environments:/ {in_env=1; next} /^---/ {in_env=0} in_env && /^ [^ ]/ {gsub(/^ /,""); gsub(/:.*$/,""); print}' "$HELMFILE")
|
environments=$(awk 'BEGIN {in_env=0} /^environments:/ {in_env=1; next} /^---/ {in_env=0} in_env && /^ [^ ]/ {gsub(/^ /,""); gsub(/:.*$/,""); print}' "$HELMFILE")
|
||||||
for env in $environments; do
|
for env in $environments; do
|
||||||
echo "################### $env lint ###################"
|
echo "################### $env lint ###################"
|
||||||
helmfile -e $env -f $HELMFILE lint || exit 1
|
helmfile -e $env lint -f $HELMFILE || exit 1
|
||||||
echo -e "\n"
|
echo -e "\n"
|
||||||
done
|
done
|
||||||
|
|||||||
10
.github/workflows/impress.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: show
|
- name: show
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
github.event_name == 'pull_request'
|
github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 50
|
fetch-depth: 50
|
||||||
- name: Check that the CHANGELOG has been modified in the current branch
|
- name: Check that the CHANGELOG has been modified in the current branch
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Check CHANGELOG max line length
|
- name: Check CHANGELOG max line length
|
||||||
run: |
|
run: |
|
||||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Install codespell
|
- name: Install codespell
|
||||||
run: pip install --user codespell
|
run: pip install --user codespell
|
||||||
- name: Check for typos
|
- name: Check for typos
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
working-directory: src/backend
|
working-directory: src/backend
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
27
.github/workflows/label_preview.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Label Preview
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: contains(github.event.pull_request.labels.*.name, 'preview')
|
||||||
|
steps:
|
||||||
|
- uses: thollander/actions-comment-pull-request@v3
|
||||||
|
with:
|
||||||
|
message: |
|
||||||
|
:rocket: Preview will be available at [https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/](https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/)
|
||||||
|
|
||||||
|
You can use the existing account with these credentials:
|
||||||
|
- username: `docs`
|
||||||
|
- password: `docs`
|
||||||
|
|
||||||
|
You can also create a new account if you want to.
|
||||||
|
|
||||||
|
Once this Pull Request is merged, the preview will be destroyed.
|
||||||
|
comment-tag: preview-url
|
||||||
3
.gitignore
vendored
@@ -75,3 +75,6 @@ db.sqlite3
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.iml
|
*.iml
|
||||||
.devcontainer
|
.devcontainer
|
||||||
|
|
||||||
|
# Cursor rules
|
||||||
|
.cursorrules
|
||||||
|
|||||||
132
CHANGELOG.md
@@ -8,10 +8,115 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(frontend) add pdf block to the editor #1293
|
- ✨ Add comments feature to the editor #1330
|
||||||
|
- ✨(backend) Comments on text editor #1330
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- ⚡️(sw) stop to cache external resources likes videos #1655
|
||||||
|
- 💥(frontend) upgrade to ui-kit v2
|
||||||
|
- ⚡️(frontend) improve perf on upload and table of contents #1662
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- ♿(frontend) improve accessibility:
|
||||||
|
- ♿(frontend) improve share modal button accessibility #1626
|
||||||
|
- ♿(frontend) improve screen reader support in DocShare modal #1628
|
||||||
|
- 🐛(frontend) fix toolbar not activated when reader #1640
|
||||||
|
- 🐛(frontend) preserve left panel width on window resize #1588
|
||||||
|
- 🐛(frontend) prevent duplicate as first character in title #1595
|
||||||
|
|
||||||
|
## [3.10.0] - 2025-11-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- ✨(export) enable ODT export for documents #1524
|
||||||
|
- ✨(frontend) improve mobile UX by showing subdocs count #1540
|
||||||
|
- ✅(e2e) add test to compare generated PDF against reference template #1648
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ♻️(frontend) preserve @ character when esc is pressed after typing it #1512
|
||||||
|
- ♻️(frontend) make summary button fixed to remain visible during scroll #1581
|
||||||
|
- ♻️(frontend) pdf embed use full width #1526
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- ♿(frontend) improve accessibility:
|
||||||
|
- ♿(frontend) improve ARIA in doc grid and editor for a11y #1519
|
||||||
|
- ♿(frontend) improve accessibility and styling of summary table #1528
|
||||||
|
- ♿(frontend) add focus trap and enter key support to remove doc modal #1531
|
||||||
|
- 🐛(frontend) fix alignment of side menu #1597
|
||||||
|
- 🐛(frontend) fix fallback translations with Trans #1620
|
||||||
|
- 🐛(export) fix image overflow by limiting width to 600px during export #1525
|
||||||
|
- 🐛(export) fix table cell alignment issue in exported documents #1582
|
||||||
|
- 🐛(export) preserve image aspect ratio in PDF export #1622
|
||||||
|
- 🐛(export) Export fails when paste with style #1552
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- mitigate role escalation in the ask_for_access viewset #1580
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- 🔥(backend) remove api managing templates
|
||||||
|
|
||||||
|
## [3.9.0] - 2025-11-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- ✨(frontend) create skeleton component for DocEditor #1491
|
||||||
|
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
|
||||||
|
- ✨(frontend) ajustable left panel #1456
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ♻️(frontend) adapt custom blocks to new implementation #1375
|
||||||
|
- ♻️(backend) increase user short_name field length #1510
|
||||||
|
- 🚸(frontend) separate viewers from editors #1509
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🐛(frontend) fix duplicate document entries in grid #1479
|
||||||
|
- 🐛(backend) fix trashbin list #1520
|
||||||
|
- ♿(frontend) improve accessibility:
|
||||||
|
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
|
||||||
|
- 🐛(backend) fix s3 version_id validation #1543
|
||||||
|
- 🐛(frontend) retry check media status after page reload #1555
|
||||||
|
- 🐛(frontend) fix Interlinking memory leak #1560
|
||||||
|
- 🐛(frontend) button new doc UI fix #1557
|
||||||
|
- 🐛(frontend) interlinking UI fix #1557
|
||||||
|
|
||||||
|
## [3.8.2] - 2025-10-17
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🐛(service-worker) fix sw registration and page reload logic #1500
|
||||||
|
|
||||||
|
## [3.8.1] - 2025-10-17
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- ⚡️(backend) improve trashbin endpoint performance #1495
|
||||||
|
- 🐛(backend) manage invitation partial update without email #1494
|
||||||
|
- ♿(frontend) improve accessibility:
|
||||||
|
- ♿ add missing aria-label to add sub-doc button for accessibility #1480
|
||||||
|
- ♿ add missing aria-label to more options button on sub-docs #1481
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- 🔥(backend) remove treebeard form for the document admin #1470
|
||||||
|
|
||||||
|
## [3.8.0] - 2025-10-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- ✨(frontend) add pdf block to the editor #1293
|
||||||
|
- ✨List and restore deleted docs #1450
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ♻️(frontend) Refactor Auth component for improved redirection logic #1461
|
||||||
- ♻️(frontend) replace Arial font-family with token font #1411
|
- ♻️(frontend) replace Arial font-family with token font #1411
|
||||||
- ♿(frontend) improve accessibility:
|
- ♿(frontend) improve accessibility:
|
||||||
- ♿(frontend) enable enter key to open documentss #1354
|
- ♿(frontend) enable enter key to open documentss #1354
|
||||||
@@ -30,10 +135,11 @@ and this project adheres to
|
|||||||
- ♿ add h1 for SR on 40X pages and remove alt texts #1438
|
- ♿ add h1 for SR on 40X pages and remove alt texts #1438
|
||||||
- ♿ update labels and shared document icon accessibility #1442
|
- ♿ update labels and shared document icon accessibility #1442
|
||||||
- 🍱(frontend) Fonts GDPR compliants #1453
|
- 🍱(frontend) Fonts GDPR compliants #1453
|
||||||
|
- ♻️(service-worker) improve SW registration and update handling #1473
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🐛(backend) duplicate sub docs as root for reader users
|
- 🐛(backend) duplicate sub docs as root for reader users #1385
|
||||||
- ⚗️(service-worker) remove index from cache first strategy #1395
|
- ⚗️(service-worker) remove index from cache first strategy #1395
|
||||||
- 🐛(frontend) fix 404 page when reload 403 page #1402
|
- 🐛(frontend) fix 404 page when reload 403 page #1402
|
||||||
- 🐛(frontend) fix legacy role computation #1376
|
- 🐛(frontend) fix legacy role computation #1376
|
||||||
@@ -44,14 +150,24 @@ and this project adheres to
|
|||||||
- 🐛(frontend) exclude h4-h6 headings from table of contents #1441
|
- 🐛(frontend) exclude h4-h6 headings from table of contents #1441
|
||||||
- 🔒(frontend) prevent readers from changing callout emoji #1449
|
- 🔒(frontend) prevent readers from changing callout emoji #1449
|
||||||
- 🐛(frontend) fix overlapping placeholders in multi-column layout #1455
|
- 🐛(frontend) fix overlapping placeholders in multi-column layout #1455
|
||||||
- 🐛(backend) filter invitation with case insensitive email
|
- 🐛(backend) filter invitation with case insensitive email #1457
|
||||||
- 🐛(frontend) reduce no access image size from 450 to 300 #1463
|
- 🐛(frontend) reduce no access image size from 450 to 300 #1463
|
||||||
|
- 🐛(frontend) preserve interlink style on drag-and-drop in editor #1460
|
||||||
|
- ✨(frontend) load docs logo from public folder via url #1462
|
||||||
|
- 🔧(keycloak) Fix https required issue in dev mode #1286
|
||||||
|
|
||||||
|
## Removed
|
||||||
|
|
||||||
|
- 🔥(frontend) remove custom DividerBlock ##1375
|
||||||
|
|
||||||
## [3.7.0] - 2025-09-12
|
## [3.7.0] - 2025-09-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(api) add API route to fetch document content #1206
|
- ✨(api) add API route to fetch document content #1206
|
||||||
|
- ✨(frontend) doc emojis improvements #1381
|
||||||
|
- add an EmojiPicker in the document tree and document title
|
||||||
|
- remove emoji buttons in menus
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -65,6 +181,8 @@ and this project adheres to
|
|||||||
- ✨unify tab focus style for better visual consistency #1341
|
- ✨unify tab focus style for better visual consistency #1341
|
||||||
- ♿hide decorative icons, label menus, avoid accessible name… #1362
|
- ♿hide decorative icons, label menus, avoid accessible name… #1362
|
||||||
- ♻️(tilt) use helm dev-backend chart
|
- ♻️(tilt) use helm dev-backend chart
|
||||||
|
- 🩹(frontend) on main pages do not display leading emoji as page icon #1381
|
||||||
|
- 🩹(frontend) handle properly emojis in interlinking #1381
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
@@ -109,7 +227,6 @@ and this project adheres to
|
|||||||
- 🐛(frontend) fix dnd conflict with tree and Blocknote #1328
|
- 🐛(frontend) fix dnd conflict with tree and Blocknote #1328
|
||||||
- 🐛(frontend) fix display bug on homepage #1332
|
- 🐛(frontend) fix display bug on homepage #1332
|
||||||
- 🐛link role update #1287
|
- 🐛link role update #1287
|
||||||
- 🔧(keycloak) Fix https required issue in dev mode #1286
|
|
||||||
|
|
||||||
## [3.5.0] - 2025-07-31
|
## [3.5.0] - 2025-07-31
|
||||||
|
|
||||||
@@ -781,7 +898,12 @@ and this project adheres to
|
|||||||
- ✨(frontend) Coming Soon page (#67)
|
- ✨(frontend) Coming Soon page (#67)
|
||||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||||
|
|
||||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.7.0...main
|
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.10.0...main
|
||||||
|
[v3.10.0]: https://github.com/suitenumerique/docs/releases/v3.10.0
|
||||||
|
[v3.9.0]: https://github.com/suitenumerique/docs/releases/v3.9.0
|
||||||
|
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
|
||||||
|
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
|
||||||
|
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
|
||||||
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
|
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
|
||||||
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
|
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
|
||||||
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
|
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
|
||||||
|
|||||||
@@ -94,6 +94,14 @@ RUN chmod g=u /etc/passwd
|
|||||||
# Copy installed python dependencies
|
# Copy installed python dependencies
|
||||||
COPY --from=back-builder /install /usr/local
|
COPY --from=back-builder /install /usr/local
|
||||||
|
|
||||||
|
# Link certifi certificate from a static path /cert/cacert.pem to avoid issues
|
||||||
|
# when python is upgraded and the path to the certificate changes.
|
||||||
|
# The space between print and the ( is intended otherwise the git lint is failing
|
||||||
|
RUN mkdir /cert && \
|
||||||
|
path=`python -c 'import certifi;print (certifi.where())'` && \
|
||||||
|
mv $path /cert/ && \
|
||||||
|
ln -s /cert/cacert.pem $path
|
||||||
|
|
||||||
# Copy impress application (see .dockerignore)
|
# Copy impress application (see .dockerignore)
|
||||||
COPY ./src/backend /app/
|
COPY ./src/backend /app/
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ backend:
|
|||||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||||
extraVolumeMounts:
|
extraVolumeMounts:
|
||||||
- name: certs
|
- name: certs
|
||||||
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
|
mountPath: /cert/cacert.pem
|
||||||
subPath: cacert.pem
|
subPath: cacert.pem
|
||||||
|
|
||||||
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
|
||||||
|
|||||||
@@ -32,6 +32,24 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
# **Your Docs icon** 📝
|
||||||
|
|
||||||
|
You can add your own Docs icon in the header from the theme customization file.
|
||||||
|
|
||||||
|
### Settings 🔧
|
||||||
|
|
||||||
|
```shellscript
|
||||||
|
THEME_CUSTOMIZATION_FILE_PATH=<path>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example of JSON
|
||||||
|
|
||||||
|
You can activate it with the `header.icon` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||||
|
|
||||||
|
This configuration is optional. If not set, the default icon will be used.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
# **Footer Configuration** 📝
|
# **Footer Configuration** 📝
|
||||||
|
|
||||||
The footer is configurable from the theme customization file.
|
The footer is configurable from the theme customization file.
|
||||||
|
|||||||
@@ -66,3 +66,6 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
|
|||||||
DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
|
DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
|
||||||
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
|
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
|
||||||
Y_PROVIDER_API_KEY=yprovider-api-key
|
Y_PROVIDER_API_KEY=yprovider-api-key
|
||||||
|
|
||||||
|
# Theme customization
|
||||||
|
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
|
||||||
@@ -19,13 +19,17 @@
|
|||||||
"matchPackageNames": ["redis"],
|
"matchPackageNames": ["redis"],
|
||||||
"allowedVersions": "<6.0.0"
|
"allowedVersions": "<6.0.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"groupName": "allowed pylint versions",
|
||||||
|
"matchManagers": ["pep621"],
|
||||||
|
"matchPackageNames": ["pylint"],
|
||||||
|
"allowedVersions": "<4.0.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"groupName": "ignored js dependencies",
|
"groupName": "ignored js dependencies",
|
||||||
"matchManagers": ["npm"],
|
"matchManagers": ["npm"],
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"@hocuspocus/provider",
|
|
||||||
"@hocuspocus/server",
|
|
||||||
"docx",
|
"docx",
|
||||||
"fetch-mock",
|
"fetch-mock",
|
||||||
"node",
|
"node",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from django.contrib.auth import admin as auth_admin
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from treebeard.admin import TreeAdmin
|
from treebeard.admin import TreeAdmin
|
||||||
from treebeard.forms import movenodeform_factory
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
@@ -157,7 +156,6 @@ class DocumentAdmin(TreeAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
form = movenodeform_factory(models.Document)
|
|
||||||
inlines = (DocumentAccessInline,)
|
inlines = (DocumentAccessInline,)
|
||||||
list_display = (
|
list_display = (
|
||||||
"id",
|
"id",
|
||||||
|
|||||||
@@ -171,3 +171,19 @@ class ResourceAccessPermission(IsAuthenticated):
|
|||||||
|
|
||||||
action = view.action
|
action = view.action
|
||||||
return abilities.get(action, False)
|
return abilities.get(action, False)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentPermission(permissions.BasePermission):
|
||||||
|
"""Permission class for comments."""
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
"""Check permission for a given object."""
|
||||||
|
if view.action in ["create", "list"]:
|
||||||
|
document_abilities = view.get_document_or_404().get_abilities(request.user)
|
||||||
|
return document_abilities["comment"]
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
"""Check permission for a given object."""
|
||||||
|
return obj.get_abilities(request.user).get(view.action, False)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Client serializers for the impress core app."""
|
"""Client serializers for the impress core app."""
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@@ -24,22 +25,13 @@ from core.services.converter_services import (
|
|||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
"""Serialize users."""
|
"""Serialize users."""
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = ["id", "email", "full_name", "short_name", "language"]
|
|
||||||
read_only_fields = ["id", "email", "full_name", "short_name"]
|
|
||||||
|
|
||||||
|
|
||||||
class UserLightSerializer(UserSerializer):
|
|
||||||
"""Serialize users with limited fields."""
|
|
||||||
|
|
||||||
full_name = serializers.SerializerMethodField(read_only=True)
|
full_name = serializers.SerializerMethodField(read_only=True)
|
||||||
short_name = serializers.SerializerMethodField(read_only=True)
|
short_name = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
fields = ["full_name", "short_name"]
|
fields = ["id", "email", "full_name", "short_name", "language"]
|
||||||
read_only_fields = ["full_name", "short_name"]
|
read_only_fields = ["id", "email", "full_name", "short_name"]
|
||||||
|
|
||||||
def get_full_name(self, instance):
|
def get_full_name(self, instance):
|
||||||
"""Return the full name of the user."""
|
"""Return the full name of the user."""
|
||||||
@@ -58,6 +50,15 @@ class UserLightSerializer(UserSerializer):
|
|||||||
return instance.short_name
|
return instance.short_name
|
||||||
|
|
||||||
|
|
||||||
|
class UserLightSerializer(UserSerializer):
|
||||||
|
"""Serialize users with limited fields."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ["full_name", "short_name"]
|
||||||
|
read_only_fields = ["full_name", "short_name"]
|
||||||
|
|
||||||
|
|
||||||
class TemplateAccessSerializer(serializers.ModelSerializer):
|
class TemplateAccessSerializer(serializers.ModelSerializer):
|
||||||
"""Serialize template accesses."""
|
"""Serialize template accesses."""
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
|||||||
nb_accesses_direct = serializers.IntegerField(read_only=True)
|
nb_accesses_direct = serializers.IntegerField(read_only=True)
|
||||||
user_role = serializers.SerializerMethodField(read_only=True)
|
user_role = serializers.SerializerMethodField(read_only=True)
|
||||||
abilities = serializers.SerializerMethodField(read_only=True)
|
abilities = serializers.SerializerMethodField(read_only=True)
|
||||||
|
deleted_at = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Document
|
model = models.Document
|
||||||
@@ -102,6 +104,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
|||||||
"computed_link_role",
|
"computed_link_role",
|
||||||
"created_at",
|
"created_at",
|
||||||
"creator",
|
"creator",
|
||||||
|
"deleted_at",
|
||||||
"depth",
|
"depth",
|
||||||
"excerpt",
|
"excerpt",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
@@ -124,6 +127,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
|||||||
"computed_link_role",
|
"computed_link_role",
|
||||||
"created_at",
|
"created_at",
|
||||||
"creator",
|
"creator",
|
||||||
|
"deleted_at",
|
||||||
"depth",
|
"depth",
|
||||||
"excerpt",
|
"excerpt",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
@@ -165,6 +169,10 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
|||||||
request = self.context.get("request")
|
request = self.context.get("request")
|
||||||
return instance.get_role(request.user) if request else None
|
return instance.get_role(request.user) if request else None
|
||||||
|
|
||||||
|
def get_deleted_at(self, instance):
|
||||||
|
"""Return the deleted_at of the current document."""
|
||||||
|
return instance.ancestors_deleted_at
|
||||||
|
|
||||||
|
|
||||||
class DocumentLightSerializer(serializers.ModelSerializer):
|
class DocumentLightSerializer(serializers.ModelSerializer):
|
||||||
"""Minial document serializer for nesting in document accesses."""
|
"""Minial document serializer for nesting in document accesses."""
|
||||||
@@ -193,6 +201,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
|||||||
"content",
|
"content",
|
||||||
"created_at",
|
"created_at",
|
||||||
"creator",
|
"creator",
|
||||||
|
"deleted_at",
|
||||||
"depth",
|
"depth",
|
||||||
"excerpt",
|
"excerpt",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
@@ -216,6 +225,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
|||||||
"computed_link_role",
|
"computed_link_role",
|
||||||
"created_at",
|
"created_at",
|
||||||
"creator",
|
"creator",
|
||||||
|
"deleted_at",
|
||||||
"depth",
|
"depth",
|
||||||
"is_favorite",
|
"is_favorite",
|
||||||
"link_role",
|
"link_role",
|
||||||
@@ -740,7 +750,8 @@ class InvitationSerializer(serializers.ModelSerializer):
|
|||||||
if self.instance is None:
|
if self.instance is None:
|
||||||
attrs["issuer"] = user
|
attrs["issuer"] = user
|
||||||
|
|
||||||
attrs["email"] = attrs["email"].lower()
|
if attrs.get("email"):
|
||||||
|
attrs["email"] = attrs["email"].lower()
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@@ -776,7 +787,9 @@ class DocumentAskForAccessCreateSerializer(serializers.Serializer):
|
|||||||
"""Serializer for creating a document ask for access."""
|
"""Serializer for creating a document ask for access."""
|
||||||
|
|
||||||
role = serializers.ChoiceField(
|
role = serializers.ChoiceField(
|
||||||
choices=models.RoleChoices.choices,
|
choices=[
|
||||||
|
role for role in choices.RoleChoices if role != models.RoleChoices.OWNER
|
||||||
|
],
|
||||||
required=False,
|
required=False,
|
||||||
default=models.RoleChoices.READER,
|
default=models.RoleChoices.READER,
|
||||||
)
|
)
|
||||||
@@ -800,11 +813,11 @@ class DocumentAskForAccessSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
read_only_fields = ["id", "document", "user", "role", "created_at", "abilities"]
|
read_only_fields = ["id", "document", "user", "role", "created_at", "abilities"]
|
||||||
|
|
||||||
def get_abilities(self, invitation) -> dict:
|
def get_abilities(self, instance) -> dict:
|
||||||
"""Return abilities of the logged-in user on the instance."""
|
"""Return abilities of the logged-in user on the instance."""
|
||||||
request = self.context.get("request")
|
request = self.context.get("request")
|
||||||
if request:
|
if request:
|
||||||
return invitation.get_abilities(request.user)
|
return instance.get_abilities(request.user)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@@ -879,3 +892,124 @@ class MoveDocumentSerializer(serializers.Serializer):
|
|||||||
choices=enums.MoveNodePositionChoices.choices,
|
choices=enums.MoveNodePositionChoices.choices,
|
||||||
default=enums.MoveNodePositionChoices.LAST_CHILD,
|
default=enums.MoveNodePositionChoices.LAST_CHILD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReactionSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serialize reactions."""
|
||||||
|
|
||||||
|
users = UserLightSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Reaction
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"emoji",
|
||||||
|
"created_at",
|
||||||
|
"users",
|
||||||
|
]
|
||||||
|
read_only_fields = ["id", "created_at", "users"]
|
||||||
|
|
||||||
|
|
||||||
|
class CommentSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serialize comments (nested under a thread) with reactions and abilities."""
|
||||||
|
|
||||||
|
user = UserLightSerializer(read_only=True)
|
||||||
|
abilities = serializers.SerializerMethodField()
|
||||||
|
reactions = ReactionSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Comment
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"body",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"reactions",
|
||||||
|
"abilities",
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"reactions",
|
||||||
|
"abilities",
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Validate comment data."""
|
||||||
|
|
||||||
|
request = self.context.get("request")
|
||||||
|
user = getattr(request, "user", None)
|
||||||
|
|
||||||
|
attrs["thread_id"] = self.context["thread_id"]
|
||||||
|
attrs["user_id"] = user.id if user else None
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def get_abilities(self, obj):
|
||||||
|
"""Return comment's abilities."""
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request:
|
||||||
|
return obj.get_abilities(request.user)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadSerializer(serializers.ModelSerializer):
|
||||||
|
"""Serialize threads in a backward compatible shape for current frontend.
|
||||||
|
|
||||||
|
We expose a flatten representation where ``content`` maps to the first
|
||||||
|
comment's body. Creating a thread requires a ``content`` field which is
|
||||||
|
stored as the first comment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
creator = UserLightSerializer(read_only=True)
|
||||||
|
abilities = serializers.SerializerMethodField(read_only=True)
|
||||||
|
body = serializers.JSONField(write_only=True, required=True)
|
||||||
|
comments = serializers.SerializerMethodField(read_only=True)
|
||||||
|
comments = CommentSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Thread
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"body",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"creator",
|
||||||
|
"abilities",
|
||||||
|
"comments",
|
||||||
|
"resolved",
|
||||||
|
"resolved_at",
|
||||||
|
"resolved_by",
|
||||||
|
"metadata",
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"creator",
|
||||||
|
"abilities",
|
||||||
|
"comments",
|
||||||
|
"resolved",
|
||||||
|
"resolved_at",
|
||||||
|
"resolved_by",
|
||||||
|
"metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
"""Validate thread data."""
|
||||||
|
request = self.context.get("request")
|
||||||
|
user = getattr(request, "user", None)
|
||||||
|
|
||||||
|
attrs["document_id"] = self.context["resource_id"]
|
||||||
|
attrs["creator_id"] = user.id if user else None
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def get_abilities(self, thread):
|
||||||
|
"""Return thread's abilities."""
|
||||||
|
request = self.context.get("request")
|
||||||
|
if request:
|
||||||
|
return thread.get_abilities(request.user)
|
||||||
|
return {}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from django.db.models.expressions import RawSQL
|
|||||||
from django.db.models.functions import Left, Length
|
from django.db.models.functions import Left, Length
|
||||||
from django.http import Http404, StreamingHttpResponse
|
from django.http import Http404, StreamingHttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.text import capfirst, slugify
|
from django.utils.text import capfirst, slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -623,12 +624,32 @@ class DocumentViewSet(
|
|||||||
The selected documents are those deleted within the cutoff period defined in the
|
The selected documents are those deleted within the cutoff period defined in the
|
||||||
settings (see TRASHBIN_CUTOFF_DAYS), before they are considered permanently deleted.
|
settings (see TRASHBIN_CUTOFF_DAYS), before they are considered permanently deleted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return self.get_response_for_queryset(self.queryset.none())
|
||||||
|
|
||||||
|
access_documents_paths = (
|
||||||
|
models.DocumentAccess.objects.select_related("document")
|
||||||
|
.filter(
|
||||||
|
db.Q(user=self.request.user) | db.Q(team__in=self.request.user.teams),
|
||||||
|
role=models.RoleChoices.OWNER,
|
||||||
|
)
|
||||||
|
.values_list("document__path", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not access_documents_paths:
|
||||||
|
return self.get_response_for_queryset(self.queryset.none())
|
||||||
|
|
||||||
|
children_clause = db.Q()
|
||||||
|
for path in access_documents_paths:
|
||||||
|
children_clause |= db.Q(path__startswith=path)
|
||||||
|
|
||||||
queryset = self.queryset.filter(
|
queryset = self.queryset.filter(
|
||||||
|
children_clause,
|
||||||
deleted_at__isnull=False,
|
deleted_at__isnull=False,
|
||||||
deleted_at__gte=models.get_trashbin_cutoff(),
|
deleted_at__gte=models.get_trashbin_cutoff(),
|
||||||
)
|
)
|
||||||
queryset = queryset.annotate_user_roles(self.request.user)
|
queryset = queryset.annotate_user_roles(self.request.user)
|
||||||
queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER])
|
|
||||||
|
|
||||||
return self.get_response_for_queryset(queryset)
|
return self.get_response_for_queryset(queryset)
|
||||||
|
|
||||||
@@ -851,33 +872,47 @@ class DocumentViewSet(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
current_document = (
|
current_document = (
|
||||||
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
|
self.queryset.select_related(None)
|
||||||
|
.only("depth", "path", "ancestors_deleted_at")
|
||||||
|
.get(pk=pk)
|
||||||
)
|
)
|
||||||
except models.Document.DoesNotExist as excpt:
|
except models.Document.DoesNotExist as excpt:
|
||||||
raise drf.exceptions.NotFound() from excpt
|
raise drf.exceptions.NotFound() from excpt
|
||||||
|
|
||||||
ancestors = (
|
is_deleted = current_document.ancestors_deleted_at is not None
|
||||||
(
|
|
||||||
current_document.get_ancestors()
|
|
||||||
| self.queryset.select_related(None).filter(pk=pk)
|
|
||||||
)
|
|
||||||
.filter(ancestors_deleted_at__isnull=True)
|
|
||||||
.order_by("path")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the highest readable ancestor
|
if is_deleted:
|
||||||
highest_readable = (
|
if current_document.get_role(user) != models.RoleChoices.OWNER:
|
||||||
ancestors.select_related(None)
|
raise (
|
||||||
.readable_per_se(request.user)
|
drf.exceptions.PermissionDenied()
|
||||||
.only("depth", "path")
|
if request.user.is_authenticated
|
||||||
.first()
|
else drf.exceptions.NotAuthenticated()
|
||||||
)
|
)
|
||||||
if highest_readable is None:
|
highest_readable = current_document
|
||||||
raise (
|
ancestors = self.queryset.select_related(None).filter(pk=pk)
|
||||||
drf.exceptions.PermissionDenied()
|
else:
|
||||||
if request.user.is_authenticated
|
ancestors = (
|
||||||
else drf.exceptions.NotAuthenticated()
|
(
|
||||||
|
current_document.get_ancestors()
|
||||||
|
| self.queryset.select_related(None).filter(pk=pk)
|
||||||
|
)
|
||||||
|
.filter(ancestors_deleted_at__isnull=True)
|
||||||
|
.order_by("path")
|
||||||
)
|
)
|
||||||
|
# Get the highest readable ancestor
|
||||||
|
highest_readable = (
|
||||||
|
ancestors.select_related(None)
|
||||||
|
.readable_per_se(request.user)
|
||||||
|
.only("depth", "path")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if highest_readable is None:
|
||||||
|
raise (
|
||||||
|
drf.exceptions.PermissionDenied()
|
||||||
|
if request.user.is_authenticated
|
||||||
|
else drf.exceptions.NotAuthenticated()
|
||||||
|
)
|
||||||
paths_links_mapping = {}
|
paths_links_mapping = {}
|
||||||
ancestors_links = []
|
ancestors_links = []
|
||||||
children_clause = db.Q()
|
children_clause = db.Q()
|
||||||
@@ -1071,7 +1106,7 @@ class DocumentViewSet(
|
|||||||
@drf.decorators.action(
|
@drf.decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["get", "delete"],
|
methods=["get", "delete"],
|
||||||
url_path="versions/(?P<version_id>[0-9a-z-]+)",
|
url_path=r"versions/(?P<version_id>[A-Za-z0-9._+\-=~]{1,1024})",
|
||||||
)
|
)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def versions_detail(self, request, pk, version_id, *args, **kwargs):
|
def versions_detail(self, request, pk, version_id, *args, **kwargs):
|
||||||
@@ -1797,10 +1832,7 @@ class DocumentAccessViewSet(
|
|||||||
|
|
||||||
|
|
||||||
class TemplateViewSet(
|
class TemplateViewSet(
|
||||||
drf.mixins.CreateModelMixin,
|
|
||||||
drf.mixins.DestroyModelMixin,
|
|
||||||
drf.mixins.RetrieveModelMixin,
|
drf.mixins.RetrieveModelMixin,
|
||||||
drf.mixins.UpdateModelMixin,
|
|
||||||
viewsets.GenericViewSet,
|
viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
"""Template ViewSet"""
|
"""Template ViewSet"""
|
||||||
@@ -1856,100 +1888,6 @@ class TemplateViewSet(
|
|||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return drf.response.Response(serializer.data)
|
return drf.response.Response(serializer.data)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
"""Set the current user as owner of the newly created object."""
|
|
||||||
obj = serializer.save()
|
|
||||||
models.TemplateAccess.objects.create(
|
|
||||||
template=obj,
|
|
||||||
user=self.request.user,
|
|
||||||
role=models.RoleChoices.OWNER,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateAccessViewSet(
|
|
||||||
ResourceAccessViewsetMixin,
|
|
||||||
drf.mixins.CreateModelMixin,
|
|
||||||
drf.mixins.DestroyModelMixin,
|
|
||||||
drf.mixins.RetrieveModelMixin,
|
|
||||||
drf.mixins.UpdateModelMixin,
|
|
||||||
viewsets.GenericViewSet,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
API ViewSet for all interactions with template accesses.
|
|
||||||
|
|
||||||
GET /api/v1.0/templates/<template_id>/accesses/:<template_access_id>
|
|
||||||
Return list of all template accesses related to the logged-in user or one
|
|
||||||
template access if an id is provided.
|
|
||||||
|
|
||||||
POST /api/v1.0/templates/<template_id>/accesses/ with expected data:
|
|
||||||
- user: str
|
|
||||||
- role: str [administrator|editor|reader]
|
|
||||||
Return newly created template access
|
|
||||||
|
|
||||||
PUT /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
|
|
||||||
- role: str [owner|admin|editor|reader]
|
|
||||||
Return updated template access
|
|
||||||
|
|
||||||
PATCH /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
|
|
||||||
- role: str [owner|admin|editor|reader]
|
|
||||||
Return partially updated template access
|
|
||||||
|
|
||||||
DELETE /api/v1.0/templates/<template_id>/accesses/<template_access_id>/
|
|
||||||
Delete targeted template access
|
|
||||||
"""
|
|
||||||
|
|
||||||
lookup_field = "pk"
|
|
||||||
permission_classes = [permissions.ResourceAccessPermission]
|
|
||||||
throttle_scope = "template_access"
|
|
||||||
queryset = models.TemplateAccess.objects.select_related("user").all()
|
|
||||||
resource_field_name = "template"
|
|
||||||
serializer_class = serializers.TemplateAccessSerializer
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def template(self):
|
|
||||||
"""Get related template from resource ID in url."""
|
|
||||||
try:
|
|
||||||
return models.Template.objects.get(pk=self.kwargs["resource_id"])
|
|
||||||
except models.Template.DoesNotExist as excpt:
|
|
||||||
raise drf.exceptions.NotFound() from excpt
|
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
"""Restrict templates returned by the list endpoint"""
|
|
||||||
user = self.request.user
|
|
||||||
teams = user.teams
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
|
|
||||||
# Limit to resource access instances related to a resource THAT also has
|
|
||||||
# a resource access instance for the logged-in user (we don't want to list
|
|
||||||
# only the resource access instances pointing to the logged-in user)
|
|
||||||
queryset = queryset.filter(
|
|
||||||
db.Q(template__accesses__user=user)
|
|
||||||
| db.Q(template__accesses__team__in=teams),
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
|
||||||
return drf.response.Response(serializer.data)
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
"""
|
|
||||||
Actually create the new template access:
|
|
||||||
- Ensures the `template_id` is explicitly set from the URL.
|
|
||||||
- If the assigned role is `OWNER`, checks that the requesting user is an owner
|
|
||||||
of the document. This is the only permission check deferred until this step;
|
|
||||||
all other access checks are handled earlier in the permission lifecycle.
|
|
||||||
"""
|
|
||||||
role = serializer.validated_data.get("role")
|
|
||||||
if (
|
|
||||||
role == choices.RoleChoices.OWNER
|
|
||||||
and self.template.get_role(self.request.user) != choices.RoleChoices.OWNER
|
|
||||||
):
|
|
||||||
raise drf.exceptions.PermissionDenied(
|
|
||||||
"Only owners of a template can assign other users as owners."
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.save(template_id=self.kwargs["resource_id"])
|
|
||||||
|
|
||||||
|
|
||||||
class InvitationViewset(
|
class InvitationViewset(
|
||||||
drf.mixins.CreateModelMixin,
|
drf.mixins.CreateModelMixin,
|
||||||
@@ -2128,7 +2066,18 @@ class DocumentAskForAccessViewSet(
|
|||||||
serializer = serializers.RoleSerializer(data=request.data)
|
serializer = serializers.RoleSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
document_ask_for_access.accept(role=serializer.validated_data.get("role"))
|
target_role = serializer.validated_data.get(
|
||||||
|
"role", document_ask_for_access.role
|
||||||
|
)
|
||||||
|
abilities = document_ask_for_access.get_abilities(request.user)
|
||||||
|
|
||||||
|
if target_role not in abilities["set_role_to"]:
|
||||||
|
return drf.response.Response(
|
||||||
|
{"detail": "You cannot accept a role higher than your own."},
|
||||||
|
status=drf.status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
document_ask_for_access.accept(role=target_role)
|
||||||
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
|
return drf.response.Response(status=drf.status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@@ -2157,6 +2106,7 @@ class ConfigView(drf.views.APIView):
|
|||||||
"LANGUAGES",
|
"LANGUAGES",
|
||||||
"LANGUAGE_CODE",
|
"LANGUAGE_CODE",
|
||||||
"SENTRY_DSN",
|
"SENTRY_DSN",
|
||||||
|
"TRASHBIN_CUTOFF_DAYS",
|
||||||
]
|
]
|
||||||
dict_settings = {}
|
dict_settings = {}
|
||||||
for setting in array_settings:
|
for setting in array_settings:
|
||||||
@@ -2201,3 +2151,132 @@ class ConfigView(drf.views.APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return theme_customization
|
return theme_customization
|
||||||
|
|
||||||
|
|
||||||
|
class CommentViewSetMixin:
|
||||||
|
"""Comment ViewSet Mixin."""
|
||||||
|
|
||||||
|
_document = None
|
||||||
|
|
||||||
|
def get_document_or_404(self):
|
||||||
|
"""Get the document related to the viewset or raise a 404 error."""
|
||||||
|
if self._document is None:
|
||||||
|
try:
|
||||||
|
self._document = models.Document.objects.get(
|
||||||
|
pk=self.kwargs["resource_id"],
|
||||||
|
)
|
||||||
|
except models.Document.DoesNotExist as e:
|
||||||
|
raise drf.exceptions.NotFound("Document not found.") from e
|
||||||
|
return self._document
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadViewSet(
|
||||||
|
ResourceAccessViewsetMixin,
|
||||||
|
CommentViewSetMixin,
|
||||||
|
drf.mixins.CreateModelMixin,
|
||||||
|
drf.mixins.ListModelMixin,
|
||||||
|
drf.mixins.RetrieveModelMixin,
|
||||||
|
drf.mixins.DestroyModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
"""Thread API: list/create threads and nested comment operations."""
|
||||||
|
|
||||||
|
permission_classes = [permissions.CommentPermission]
|
||||||
|
pagination_class = Pagination
|
||||||
|
serializer_class = serializers.ThreadSerializer
|
||||||
|
queryset = models.Thread.objects.select_related("creator", "document").filter(
|
||||||
|
resolved=False
|
||||||
|
)
|
||||||
|
resource_field_name = "document"
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""Create the first comment of the thread."""
|
||||||
|
body = serializer.validated_data["body"]
|
||||||
|
del serializer.validated_data["body"]
|
||||||
|
thread = serializer.save()
|
||||||
|
|
||||||
|
models.Comment.objects.create(
|
||||||
|
thread=thread,
|
||||||
|
user=self.request.user if self.request.user.is_authenticated else None,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
@drf.decorators.action(detail=True, methods=["post"], url_path="resolve")
|
||||||
|
def resolve(self, request, *args, **kwargs):
|
||||||
|
"""Resolve a thread."""
|
||||||
|
thread = self.get_object()
|
||||||
|
if not thread.resolved:
|
||||||
|
thread.resolved = True
|
||||||
|
thread.resolved_at = timezone.now()
|
||||||
|
thread.resolved_by = request.user
|
||||||
|
thread.save(update_fields=["resolved", "resolved_at", "resolved_by"])
|
||||||
|
return drf.response.Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentViewSet(
|
||||||
|
CommentViewSetMixin,
|
||||||
|
viewsets.ModelViewSet,
|
||||||
|
):
|
||||||
|
"""Comment API: list/create comments and nested reaction operations."""
|
||||||
|
|
||||||
|
permission_classes = [permissions.CommentPermission]
|
||||||
|
pagination_class = Pagination
|
||||||
|
serializer_class = serializers.CommentSerializer
|
||||||
|
queryset = models.Comment.objects.select_related("user").all()
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Override to filter on related resource."""
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
thread=self.kwargs["thread_id"],
|
||||||
|
thread__document=self.kwargs["resource_id"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
"""Extra context provided to the serializer class."""
|
||||||
|
context = super().get_serializer_context()
|
||||||
|
context["document_id"] = self.kwargs["resource_id"]
|
||||||
|
context["thread_id"] = self.kwargs["thread_id"]
|
||||||
|
return context
|
||||||
|
|
||||||
|
@drf.decorators.action(
|
||||||
|
detail=True,
|
||||||
|
methods=["post", "delete"],
|
||||||
|
)
|
||||||
|
def reactions(self, request, *args, **kwargs):
|
||||||
|
"""POST: add reaction; DELETE: remove reaction.
|
||||||
|
|
||||||
|
Emoji is expected in request.data['emoji'] for both operations.
|
||||||
|
"""
|
||||||
|
comment = self.get_object()
|
||||||
|
serializer = serializers.ReactionSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
reaction, created = models.Reaction.objects.get_or_create(
|
||||||
|
comment=comment,
|
||||||
|
emoji=serializer.validated_data["emoji"],
|
||||||
|
)
|
||||||
|
if not created and reaction.users.filter(id=request.user.id).exists():
|
||||||
|
return drf.response.Response(
|
||||||
|
{"user_already_reacted": True}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
reaction.users.add(request.user)
|
||||||
|
return drf.response.Response(status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# DELETE
|
||||||
|
try:
|
||||||
|
reaction = models.Reaction.objects.get(
|
||||||
|
comment=comment,
|
||||||
|
emoji=serializer.validated_data["emoji"],
|
||||||
|
users__in=[request.user],
|
||||||
|
)
|
||||||
|
except models.Reaction.DoesNotExist as e:
|
||||||
|
raise drf.exceptions.NotFound("Reaction not found.") from e
|
||||||
|
reaction.users.remove(request.user)
|
||||||
|
if not reaction.users.exists():
|
||||||
|
reaction.delete()
|
||||||
|
return drf.response.Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class LinkRoleChoices(PriorityTextChoices):
|
|||||||
"""Defines the possible roles a link can offer on a document."""
|
"""Defines the possible roles a link can offer on a document."""
|
||||||
|
|
||||||
READER = "reader", _("Reader") # Can read
|
READER = "reader", _("Reader") # Can read
|
||||||
|
COMMENTER = "commenter", _("Commenter") # Can read and comment
|
||||||
EDITOR = "editor", _("Editor") # Can read and edit
|
EDITOR = "editor", _("Editor") # Can read and edit
|
||||||
|
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ class RoleChoices(PriorityTextChoices):
|
|||||||
"""Defines the possible roles a user can have in a resource."""
|
"""Defines the possible roles a user can have in a resource."""
|
||||||
|
|
||||||
READER = "reader", _("Reader") # Can read
|
READER = "reader", _("Reader") # Can read
|
||||||
|
COMMENTER = "commenter", _("Commenter") # Can read and comment
|
||||||
EDITOR = "editor", _("Editor") # Can read and edit
|
EDITOR = "editor", _("Editor") # Can read and edit
|
||||||
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
|
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
|
||||||
OWNER = "owner", _("Owner")
|
OWNER = "owner", _("Owner")
|
||||||
|
|||||||
@@ -256,3 +256,49 @@ class InvitationFactory(factory.django.DjangoModelFactory):
|
|||||||
document = factory.SubFactory(DocumentFactory)
|
document = factory.SubFactory(DocumentFactory)
|
||||||
role = factory.fuzzy.FuzzyChoice([role[0] for role in models.RoleChoices.choices])
|
role = factory.fuzzy.FuzzyChoice([role[0] for role in models.RoleChoices.choices])
|
||||||
issuer = factory.SubFactory(UserFactory)
|
issuer = factory.SubFactory(UserFactory)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""A factory to create threads for a document"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Thread
|
||||||
|
|
||||||
|
document = factory.SubFactory(DocumentFactory)
|
||||||
|
creator = factory.SubFactory(UserFactory)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""A factory to create comments for a thread"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Comment
|
||||||
|
|
||||||
|
thread = factory.SubFactory(ThreadFactory)
|
||||||
|
user = factory.SubFactory(UserFactory)
|
||||||
|
body = factory.Faker("text")
|
||||||
|
|
||||||
|
|
||||||
|
class ReactionFactory(factory.django.DjangoModelFactory):
|
||||||
|
"""A factory to create reactions for a comment"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Reaction
|
||||||
|
|
||||||
|
comment = factory.SubFactory(CommentFactory)
|
||||||
|
emoji = "test"
|
||||||
|
|
||||||
|
@factory.post_generation
|
||||||
|
def users(self, create, extracted, **kwargs):
|
||||||
|
"""Add users to reaction from a given list of users or create one if not provided."""
|
||||||
|
if not create:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not extracted:
|
||||||
|
# the factory is being created, but no users were provided
|
||||||
|
user = UserFactory()
|
||||||
|
self.users.add(user)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add the iterable of groups using bulk addition
|
||||||
|
self.users.add(*extracted)
|
||||||
|
|||||||
19
src/backend/core/migrations/0025_alter_user_short_name.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-22 06:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("core", "0024_add_is_masked_field_to_link_trace"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="short_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=100, null=True, verbose_name="short name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
275
src/backend/core/migrations/0026_comments.py
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-09-16 08:59
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("core", "0025_alter_user_short_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="document",
|
||||||
|
name="link_role",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("reader", "Reader"),
|
||||||
|
("commenter", "Commenter"),
|
||||||
|
("editor", "Editor"),
|
||||||
|
],
|
||||||
|
default="reader",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="documentaccess",
|
||||||
|
name="role",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("reader", "Reader"),
|
||||||
|
("commenter", "Commenter"),
|
||||||
|
("editor", "Editor"),
|
||||||
|
("administrator", "Administrator"),
|
||||||
|
("owner", "Owner"),
|
||||||
|
],
|
||||||
|
default="reader",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="documentaskforaccess",
|
||||||
|
name="role",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("reader", "Reader"),
|
||||||
|
("commenter", "Commenter"),
|
||||||
|
("editor", "Editor"),
|
||||||
|
("administrator", "Administrator"),
|
||||||
|
("owner", "Owner"),
|
||||||
|
],
|
||||||
|
default="reader",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="invitation",
|
||||||
|
name="role",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("reader", "Reader"),
|
||||||
|
("commenter", "Commenter"),
|
||||||
|
("editor", "Editor"),
|
||||||
|
("administrator", "Administrator"),
|
||||||
|
("owner", "Owner"),
|
||||||
|
],
|
||||||
|
default="reader",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="templateaccess",
|
||||||
|
name="role",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("reader", "Reader"),
|
||||||
|
("commenter", "Commenter"),
|
||||||
|
("editor", "Editor"),
|
||||||
|
("administrator", "Administrator"),
|
||||||
|
("owner", "Owner"),
|
||||||
|
],
|
||||||
|
default="reader",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Thread",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
help_text="primary key for the record as UUID",
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="id",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
help_text="date and time at which a record was created",
|
||||||
|
verbose_name="created on",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
help_text="date and time at which a record was last updated",
|
||||||
|
verbose_name="updated on",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("resolved", models.BooleanField(default=False)),
|
||||||
|
("resolved_at", models.DateTimeField(blank=True, null=True)),
|
||||||
|
("metadata", models.JSONField(blank=True, default=dict)),
|
||||||
|
(
|
||||||
|
"creator",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="threads",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"document",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="threads",
|
||||||
|
to="core.document",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"resolved_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="resolved_threads",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Thread",
|
||||||
|
"verbose_name_plural": "Threads",
|
||||||
|
"db_table": "impress_thread",
|
||||||
|
"ordering": ("-created_at",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Comment",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
help_text="primary key for the record as UUID",
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="id",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
help_text="date and time at which a record was created",
|
||||||
|
verbose_name="created on",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
help_text="date and time at which a record was last updated",
|
||||||
|
verbose_name="updated on",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("body", models.JSONField()),
|
||||||
|
("metadata", models.JSONField(blank=True, default=dict)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="thread_comment",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"thread",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="comments",
|
||||||
|
to="core.thread",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Comment",
|
||||||
|
"verbose_name_plural": "Comments",
|
||||||
|
"db_table": "impress_comment",
|
||||||
|
"ordering": ("created_at",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Reaction",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
help_text="primary key for the record as UUID",
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="id",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
help_text="date and time at which a record was created",
|
||||||
|
verbose_name="created on",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"updated_at",
|
||||||
|
models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
help_text="date and time at which a record was last updated",
|
||||||
|
verbose_name="updated on",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("emoji", models.CharField(max_length=32)),
|
||||||
|
(
|
||||||
|
"comment",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="reactions",
|
||||||
|
to="core.comment",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"users",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="reactions", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Reaction",
|
||||||
|
"verbose_name_plural": "Reactions",
|
||||||
|
"db_table": "impress_comment_reaction",
|
||||||
|
"constraints": [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("comment", "emoji"),
|
||||||
|
name="unique_comment_emoji",
|
||||||
|
violation_error_message="This emoji has already been reacted to this comment.",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -148,7 +148,9 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
|
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
|
||||||
short_name = models.CharField(_("short name"), max_length=20, null=True, blank=True)
|
short_name = models.CharField(
|
||||||
|
_("short name"), max_length=100, null=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
||||||
|
|
||||||
@@ -721,7 +723,7 @@ class Document(MP_Node, BaseModel):
|
|||||||
|
|
||||||
# Characteristics that are based only on specific access
|
# Characteristics that are based only on specific access
|
||||||
is_owner = role == RoleChoices.OWNER
|
is_owner = role == RoleChoices.OWNER
|
||||||
is_deleted = self.ancestors_deleted_at and not is_owner
|
is_deleted = self.ancestors_deleted_at
|
||||||
is_owner_or_admin = (is_owner or role == RoleChoices.ADMIN) and not is_deleted
|
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
|
# Compute access roles before adding link roles because we don't
|
||||||
@@ -750,15 +752,17 @@ class Document(MP_Node, BaseModel):
|
|||||||
role = RoleChoices.max(role, link_definition["link_role"])
|
role = RoleChoices.max(role, link_definition["link_role"])
|
||||||
|
|
||||||
can_get = bool(role) and not is_deleted
|
can_get = bool(role) and not is_deleted
|
||||||
|
retrieve = can_get or is_owner
|
||||||
can_update = (
|
can_update = (
|
||||||
is_owner_or_admin or role == RoleChoices.EDITOR
|
is_owner_or_admin or role == RoleChoices.EDITOR
|
||||||
) and not is_deleted
|
) and not is_deleted
|
||||||
|
can_comment = (can_update or role == RoleChoices.COMMENTER) and not is_deleted
|
||||||
can_create_children = can_update and user.is_authenticated
|
can_create_children = can_update and user.is_authenticated
|
||||||
can_destroy = (
|
can_destroy = (
|
||||||
is_owner
|
is_owner
|
||||||
if self.is_root()
|
if self.is_root()
|
||||||
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
|
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
|
||||||
)
|
) and not is_deleted
|
||||||
|
|
||||||
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
|
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
|
||||||
ai_access = any(
|
ai_access = any(
|
||||||
@@ -783,6 +787,7 @@ class Document(MP_Node, BaseModel):
|
|||||||
"children_list": can_get,
|
"children_list": can_get,
|
||||||
"children_create": can_create_children,
|
"children_create": can_create_children,
|
||||||
"collaboration_auth": can_get,
|
"collaboration_auth": can_get,
|
||||||
|
"comment": can_comment,
|
||||||
"content": can_get,
|
"content": can_get,
|
||||||
"cors_proxy": can_get,
|
"cors_proxy": can_get,
|
||||||
"descendants": can_get,
|
"descendants": can_get,
|
||||||
@@ -790,15 +795,15 @@ class Document(MP_Node, BaseModel):
|
|||||||
"duplicate": can_get and user.is_authenticated,
|
"duplicate": can_get and user.is_authenticated,
|
||||||
"favorite": can_get and user.is_authenticated,
|
"favorite": can_get and user.is_authenticated,
|
||||||
"link_configuration": is_owner_or_admin,
|
"link_configuration": is_owner_or_admin,
|
||||||
"invite_owner": is_owner,
|
"invite_owner": is_owner and not is_deleted,
|
||||||
"mask": can_get and user.is_authenticated,
|
"mask": can_get and user.is_authenticated,
|
||||||
"move": is_owner_or_admin and not self.ancestors_deleted_at,
|
"move": is_owner_or_admin and not is_deleted,
|
||||||
"partial_update": can_update,
|
"partial_update": can_update,
|
||||||
"restore": is_owner,
|
"restore": is_owner,
|
||||||
"retrieve": can_get,
|
"retrieve": retrieve,
|
||||||
"media_auth": can_get,
|
"media_auth": can_get,
|
||||||
"link_select_options": link_select_options,
|
"link_select_options": link_select_options,
|
||||||
"tree": can_get,
|
"tree": retrieve,
|
||||||
"update": can_update,
|
"update": can_update,
|
||||||
"versions_destroy": is_owner_or_admin,
|
"versions_destroy": is_owner_or_admin,
|
||||||
"versions_list": has_access_role,
|
"versions_list": has_access_role,
|
||||||
@@ -1143,7 +1148,12 @@ class DocumentAccess(BaseAccess):
|
|||||||
set_role_to = []
|
set_role_to = []
|
||||||
if is_owner_or_admin:
|
if is_owner_or_admin:
|
||||||
set_role_to.extend(
|
set_role_to.extend(
|
||||||
[RoleChoices.READER, RoleChoices.EDITOR, RoleChoices.ADMIN]
|
[
|
||||||
|
RoleChoices.READER,
|
||||||
|
RoleChoices.COMMENTER,
|
||||||
|
RoleChoices.EDITOR,
|
||||||
|
RoleChoices.ADMIN,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
if role == RoleChoices.OWNER:
|
if role == RoleChoices.OWNER:
|
||||||
set_role_to.append(RoleChoices.OWNER)
|
set_role_to.append(RoleChoices.OWNER)
|
||||||
@@ -1202,23 +1212,14 @@ class DocumentAskForAccess(BaseModel):
|
|||||||
|
|
||||||
def get_abilities(self, user):
|
def get_abilities(self, user):
|
||||||
"""Compute and return abilities for a given user."""
|
"""Compute and return abilities for a given user."""
|
||||||
roles = []
|
user_role = self.document.get_role(user)
|
||||||
|
is_admin_or_owner = user_role in PRIVILEGED_ROLES
|
||||||
|
|
||||||
if user.is_authenticated:
|
set_role_to = [
|
||||||
teams = user.teams
|
role
|
||||||
try:
|
for role in RoleChoices.values
|
||||||
roles = self.user_roles or []
|
if RoleChoices.get_priority(role) <= RoleChoices.get_priority(user_role)
|
||||||
except AttributeError:
|
]
|
||||||
try:
|
|
||||||
roles = self.document.accesses.filter(
|
|
||||||
models.Q(user=user) | models.Q(team__in=teams),
|
|
||||||
).values_list("role", flat=True)
|
|
||||||
except (self._meta.model.DoesNotExist, IndexError):
|
|
||||||
roles = []
|
|
||||||
|
|
||||||
is_admin_or_owner = bool(
|
|
||||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"destroy": is_admin_or_owner,
|
"destroy": is_admin_or_owner,
|
||||||
@@ -1226,6 +1227,7 @@ class DocumentAskForAccess(BaseModel):
|
|||||||
"partial_update": is_admin_or_owner,
|
"partial_update": is_admin_or_owner,
|
||||||
"retrieve": is_admin_or_owner,
|
"retrieve": is_admin_or_owner,
|
||||||
"accept": is_admin_or_owner,
|
"accept": is_admin_or_owner,
|
||||||
|
"set_role_to": set_role_to,
|
||||||
}
|
}
|
||||||
|
|
||||||
def accept(self, role=None):
|
def accept(self, role=None):
|
||||||
@@ -1275,6 +1277,153 @@ class DocumentAskForAccess(BaseModel):
|
|||||||
self.document.send_email(subject, [email], context, language)
|
self.document.send_email(subject, [email], context, language)
|
||||||
|
|
||||||
|
|
||||||
|
class Thread(BaseModel):
|
||||||
|
"""Discussion thread attached to a document.
|
||||||
|
|
||||||
|
A thread groups one or many comments. For backward compatibility with the
|
||||||
|
existing frontend (useComments hook) we still expose a flattened serializer
|
||||||
|
that returns a "content" field representing the first comment's body.
|
||||||
|
"""
|
||||||
|
|
||||||
|
document = models.ForeignKey(
|
||||||
|
Document,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="threads",
|
||||||
|
)
|
||||||
|
creator = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="threads",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
resolved = models.BooleanField(default=False)
|
||||||
|
resolved_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
resolved_by = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="resolved_threads",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "impress_thread"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
verbose_name = _("Thread")
|
||||||
|
verbose_name_plural = _("Threads")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
author = self.creator or _("Anonymous")
|
||||||
|
return f"Thread by {author!s} on {self.document!s}"
|
||||||
|
|
||||||
|
def get_abilities(self, user):
|
||||||
|
"""Compute and return abilities for a given user (mirrors comment logic)."""
|
||||||
|
role = self.document.get_role(user)
|
||||||
|
doc_abilities = self.document.get_abilities(user)
|
||||||
|
read_access = doc_abilities.get("comment", False)
|
||||||
|
write_access = self.creator == user or role in [
|
||||||
|
RoleChoices.OWNER,
|
||||||
|
RoleChoices.ADMIN,
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"destroy": write_access,
|
||||||
|
"update": write_access,
|
||||||
|
"partial_update": write_access,
|
||||||
|
"resolve": write_access,
|
||||||
|
"retrieve": read_access,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def first_comment(self):
|
||||||
|
"""Return the first createdcomment of the thread."""
|
||||||
|
return self.comments.order_by("created_at").first()
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(BaseModel):
|
||||||
|
"""A comment belonging to a thread."""
|
||||||
|
|
||||||
|
thread = models.ForeignKey(
|
||||||
|
Thread,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="comments",
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="thread_comment",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
body = models.JSONField()
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "impress_comment"
|
||||||
|
ordering = ("created_at",)
|
||||||
|
verbose_name = _("Comment")
|
||||||
|
verbose_name_plural = _("Comments")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return the string representation of the comment."""
|
||||||
|
author = self.user or _("Anonymous")
|
||||||
|
return f"Comment by {author!s} on thread {self.thread_id}"
|
||||||
|
|
||||||
|
def get_abilities(self, user):
|
||||||
|
"""Return the abilities of the comment."""
|
||||||
|
role = self.thread.document.get_role(user)
|
||||||
|
doc_abilities = self.thread.document.get_abilities(user)
|
||||||
|
read_access = doc_abilities.get("comment", False)
|
||||||
|
can_react = read_access and user.is_authenticated
|
||||||
|
write_access = self.user == user or role in [
|
||||||
|
RoleChoices.OWNER,
|
||||||
|
RoleChoices.ADMIN,
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"destroy": write_access,
|
||||||
|
"update": write_access,
|
||||||
|
"partial_update": write_access,
|
||||||
|
"reactions": can_react,
|
||||||
|
"retrieve": read_access,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Reaction(BaseModel):
|
||||||
|
"""Aggregated reactions for a given emoji on a comment.
|
||||||
|
|
||||||
|
We store one row per (comment, emoji) and maintain the list of user IDs who
|
||||||
|
reacted with that emoji. This matches the frontend interface where a
|
||||||
|
reaction exposes: emoji, createdAt (first reaction date) and userIds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
comment = models.ForeignKey(
|
||||||
|
Comment,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="reactions",
|
||||||
|
)
|
||||||
|
emoji = models.CharField(max_length=32)
|
||||||
|
users = models.ManyToManyField(User, related_name="reactions")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "impress_comment_reaction"
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["comment", "emoji"],
|
||||||
|
name="unique_comment_emoji",
|
||||||
|
violation_error_message=_(
|
||||||
|
"This emoji has already been reacted to this comment."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
verbose_name = _("Reaction")
|
||||||
|
verbose_name_plural = _("Reactions")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return the string representation of the reaction."""
|
||||||
|
return f"Reaction {self.emoji} on comment {self.comment.id}"
|
||||||
|
|
||||||
|
|
||||||
class Template(BaseModel):
|
class Template(BaseModel):
|
||||||
"""HTML and CSS code used for formatting the print around the MarkDown body."""
|
"""HTML and CSS code used for formatting the print around the MarkDown body."""
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def test_authentication_getter_existing_user_via_email(
|
|||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
with django_assert_num_queries(3): # user by sub, user by mail, update sub
|
with django_assert_num_queries(4): # user by sub, user by mail, update sub
|
||||||
user = klass.get_or_create_user(
|
user = klass.get_or_create_user(
|
||||||
access_token="test-token", id_token=None, payload=None
|
access_token="test-token", id_token=None, payload=None
|
||||||
)
|
)
|
||||||
@@ -214,7 +214,7 @@ def test_authentication_getter_existing_user_change_fields_sub(
|
|||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
# One and only one additional update query when a field has changed
|
# One and only one additional update query when a field has changed
|
||||||
with django_assert_num_queries(2):
|
with django_assert_num_queries(3):
|
||||||
authenticated_user = klass.get_or_create_user(
|
authenticated_user = klass.get_or_create_user(
|
||||||
access_token="test-token", id_token=None, payload=None
|
access_token="test-token", id_token=None, payload=None
|
||||||
)
|
)
|
||||||
@@ -256,7 +256,7 @@ def test_authentication_getter_existing_user_change_fields_email(
|
|||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
# One and only one additional update query when a field has changed
|
# One and only one additional update query when a field has changed
|
||||||
with django_assert_num_queries(3):
|
with django_assert_num_queries(4):
|
||||||
authenticated_user = klass.get_or_create_user(
|
authenticated_user = klass.get_or_create_user(
|
||||||
access_token="test-token", id_token=None, payload=None
|
access_token="test-token", id_token=None, payload=None
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
|
|||||||
}
|
}
|
||||||
assert result_dict[str(document_access_other_user.id)] == [
|
assert result_dict[str(document_access_other_user.id)] == [
|
||||||
"reader",
|
"reader",
|
||||||
|
"commenter",
|
||||||
"editor",
|
"editor",
|
||||||
"administrator",
|
"administrator",
|
||||||
"owner",
|
"owner",
|
||||||
@@ -301,7 +302,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
|
|||||||
|
|
||||||
# Add an access for the other user on the parent
|
# Add an access for the other user on the parent
|
||||||
parent_access_other_user = factories.UserDocumentAccessFactory(
|
parent_access_other_user = factories.UserDocumentAccessFactory(
|
||||||
document=parent, user=other_user, role="editor"
|
document=parent, user=other_user, role="commenter"
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/accesses/")
|
||||||
@@ -314,6 +315,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
|
|||||||
result["id"]: result["abilities"]["set_role_to"] for result in content
|
result["id"]: result["abilities"]["set_role_to"] for result in content
|
||||||
}
|
}
|
||||||
assert result_dict[str(document_access_other_user.id)] == [
|
assert result_dict[str(document_access_other_user.id)] == [
|
||||||
|
"commenter",
|
||||||
"editor",
|
"editor",
|
||||||
"administrator",
|
"administrator",
|
||||||
"owner",
|
"owner",
|
||||||
@@ -321,6 +323,7 @@ def test_api_document_accesses_retrieve_set_role_to_child():
|
|||||||
assert result_dict[str(parent_access.id)] == []
|
assert result_dict[str(parent_access.id)] == []
|
||||||
assert result_dict[str(parent_access_other_user.id)] == [
|
assert result_dict[str(parent_access_other_user.id)] == [
|
||||||
"reader",
|
"reader",
|
||||||
|
"commenter",
|
||||||
"editor",
|
"editor",
|
||||||
"administrator",
|
"administrator",
|
||||||
"owner",
|
"owner",
|
||||||
@@ -333,28 +336,28 @@ def test_api_document_accesses_retrieve_set_role_to_child():
|
|||||||
[
|
[
|
||||||
["administrator", "reader", "reader", "reader"],
|
["administrator", "reader", "reader", "reader"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator"],
|
["reader", "commenter", "editor", "administrator"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
["reader", "editor", "administrator"],
|
["reader", "commenter", "editor", "administrator"],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
["owner", "reader", "reader", "reader"],
|
["owner", "reader", "reader", "reader"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
["owner", "reader", "reader", "owner"],
|
["owner", "reader", "reader", "owner"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -415,44 +418,44 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
|
|||||||
[
|
[
|
||||||
["administrator", "reader", "reader", "reader"],
|
["administrator", "reader", "reader", "reader"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator"],
|
["reader", "commenter", "editor", "administrator"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
["reader", "editor", "administrator"],
|
["reader", "commenter", "editor", "administrator"],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
["owner", "reader", "reader", "reader"],
|
["owner", "reader", "reader", "reader"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
["owner", "reader", "reader", "owner"],
|
["owner", "reader", "reader", "owner"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
["reader", "reader", "reader", "owner"],
|
["reader", "reader", "reader", "owner"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
["reader", "editor", "administrator", "owner"],
|
["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
["reader", "administrator", "reader", "editor"],
|
["reader", "administrator", "reader", "editor"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator"],
|
["reader", "commenter", "editor", "administrator"],
|
||||||
["reader", "editor", "administrator"],
|
["reader", "commenter", "editor", "administrator"],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
],
|
],
|
||||||
@@ -460,7 +463,7 @@ def test_api_document_accesses_list_authenticated_related_same_user(roles, resul
|
|||||||
[
|
[
|
||||||
["editor", "editor", "administrator", "editor"],
|
["editor", "editor", "administrator", "editor"],
|
||||||
[
|
[
|
||||||
["reader", "editor", "administrator"],
|
["reader", "commenter", "editor", "administrator"],
|
||||||
[],
|
[],
|
||||||
["editor", "administrator"],
|
["editor", "administrator"],
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -769,6 +769,37 @@ def test_api_document_invitations_update_authenticated_unprivileged(
|
|||||||
assert value == old_invitation_values[key]
|
assert value == old_invitation_values[key]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("via", VIA)
|
||||||
|
@pytest.mark.parametrize("role", ["administrator", "owner"])
|
||||||
|
def test_api_document_invitations_patch(via, role, mock_user_teams):
|
||||||
|
"""Partially updating an invitation should be allowed."""
|
||||||
|
|
||||||
|
user = factories.UserFactory()
|
||||||
|
invitation = factories.InvitationFactory(role="editor")
|
||||||
|
|
||||||
|
if via == USER:
|
||||||
|
factories.UserDocumentAccessFactory(
|
||||||
|
document=invitation.document, user=user, role=role
|
||||||
|
)
|
||||||
|
elif via == TEAM:
|
||||||
|
mock_user_teams.return_value = ["lasuite", "unknown"]
|
||||||
|
factories.TeamDocumentAccessFactory(
|
||||||
|
document=invitation.document, team="lasuite", role=role
|
||||||
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/api/v1.0/documents/{invitation.document.id!s}/invitations/{invitation.id!s}/",
|
||||||
|
{"role": "reader"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
invitation.refresh_from_db()
|
||||||
|
assert invitation.role == "reader"
|
||||||
|
|
||||||
|
|
||||||
# Delete
|
# Delete
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ def test_api_documents_ask_for_access_create_authenticated_non_root_document():
|
|||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_ask_for_access_create_authenticated_specific_role():
|
@pytest.mark.parametrize(
|
||||||
|
"role", [role for role in RoleChoices if role != RoleChoices.OWNER]
|
||||||
|
)
|
||||||
|
def test_api_documents_ask_for_access_create_authenticated_specific_role(role):
|
||||||
"""
|
"""
|
||||||
Authenticated users should be able to create a document ask for access with a specific role.
|
Authenticated users should be able to create a document ask for access with a specific role.
|
||||||
"""
|
"""
|
||||||
@@ -127,17 +130,35 @@ def test_api_documents_ask_for_access_create_authenticated_specific_role():
|
|||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f"/api/v1.0/documents/{document.id}/ask-for-access/",
|
f"/api/v1.0/documents/{document.id}/ask-for-access/",
|
||||||
data={"role": RoleChoices.EDITOR},
|
data={"role": role},
|
||||||
)
|
)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
assert DocumentAskForAccess.objects.filter(
|
assert DocumentAskForAccess.objects.filter(
|
||||||
document=document,
|
document=document,
|
||||||
user=user,
|
user=user,
|
||||||
role=RoleChoices.EDITOR,
|
role=role,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_ask_for_access_create_authenticated_owner_role():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to create a document ask for access with the owner role.
|
||||||
|
"""
|
||||||
|
document = DocumentFactory()
|
||||||
|
user = UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id}/ask-for-access/",
|
||||||
|
data={"role": RoleChoices.OWNER},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json() == {"role": ['"owner" is not a valid choice.']}
|
||||||
|
|
||||||
|
|
||||||
def test_api_documents_ask_for_access_create_authenticated_already_has_access():
|
def test_api_documents_ask_for_access_create_authenticated_already_has_access():
|
||||||
"""Authenticated users with existing access can ask for access with a different role."""
|
"""Authenticated users with existing access can ask for access with a different role."""
|
||||||
user = UserFactory()
|
user = UserFactory()
|
||||||
@@ -266,6 +287,7 @@ def test_api_documents_ask_for_access_list_authenticated_own_request():
|
|||||||
"update": False,
|
"update": False,
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
"retrieve": False,
|
"retrieve": False,
|
||||||
|
"set_role_to": [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -335,6 +357,16 @@ def test_api_documents_ask_for_access_list_owner_or_admin(role):
|
|||||||
|
|
||||||
response = client.get(f"/api/v1.0/documents/{document.id}/ask-for-access/")
|
response = client.get(f"/api/v1.0/documents/{document.id}/ask-for-access/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
expected_set_role_to = [
|
||||||
|
RoleChoices.READER,
|
||||||
|
RoleChoices.COMMENTER,
|
||||||
|
RoleChoices.EDITOR,
|
||||||
|
RoleChoices.ADMIN,
|
||||||
|
]
|
||||||
|
if role == RoleChoices.OWNER:
|
||||||
|
expected_set_role_to.append(RoleChoices.OWNER)
|
||||||
|
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"count": 3,
|
"count": 3,
|
||||||
"next": None,
|
"next": None,
|
||||||
@@ -354,6 +386,7 @@ def test_api_documents_ask_for_access_list_owner_or_admin(role):
|
|||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
|
"set_role_to": expected_set_role_to,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for document_ask_for_access in document_ask_for_accesses
|
for document_ask_for_access in document_ask_for_accesses
|
||||||
@@ -446,6 +479,14 @@ def test_api_documents_ask_for_access_retrieve_owner_or_admin(role):
|
|||||||
f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/"
|
f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/"
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
expected_set_role_to = [
|
||||||
|
RoleChoices.READER,
|
||||||
|
RoleChoices.COMMENTER,
|
||||||
|
RoleChoices.EDITOR,
|
||||||
|
RoleChoices.ADMIN,
|
||||||
|
]
|
||||||
|
if role == RoleChoices.OWNER:
|
||||||
|
expected_set_role_to.append(RoleChoices.OWNER)
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"id": str(document_ask_for_access.id),
|
"id": str(document_ask_for_access.id),
|
||||||
"document": str(document.id),
|
"document": str(document.id),
|
||||||
@@ -460,6 +501,7 @@ def test_api_documents_ask_for_access_retrieve_owner_or_admin(role):
|
|||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
|
"set_role_to": expected_set_role_to,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,6 +791,53 @@ def test_api_documents_ask_for_access_accept_authenticated_owner_or_admin_update
|
|||||||
assert document_access.role == RoleChoices.ADMIN
|
assert document_access.role == RoleChoices.ADMIN
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_ask_for_access_accept_admin_cannot_accept_owner_role():
|
||||||
|
"""
|
||||||
|
Admin users should not be able to accept document ask for access with the owner role.
|
||||||
|
"""
|
||||||
|
user = UserFactory()
|
||||||
|
document = DocumentFactory(users=[(user, RoleChoices.ADMIN)])
|
||||||
|
document_ask_for_access = DocumentAskForAccessFactory(
|
||||||
|
document=document, role=RoleChoices.READER
|
||||||
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/",
|
||||||
|
data={"role": RoleChoices.OWNER},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": "You cannot accept a role higher than your own."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_ask_for_access_accept_owner_can_accept_owner_role():
|
||||||
|
"""
|
||||||
|
Owner users should be able to accept document ask for access with the owner role.
|
||||||
|
"""
|
||||||
|
user = UserFactory()
|
||||||
|
document = DocumentFactory(users=[(user, RoleChoices.OWNER)])
|
||||||
|
document_ask_for_access = DocumentAskForAccessFactory(
|
||||||
|
document=document, role=RoleChoices.READER
|
||||||
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id}/ask-for-access/{document_ask_for_access.id}/accept/",
|
||||||
|
data={"role": RoleChoices.OWNER},
|
||||||
|
)
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
assert not DocumentAskForAccess.objects.filter(
|
||||||
|
id=document_ask_for_access.id
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
|
@pytest.mark.parametrize("role", [RoleChoices.OWNER, RoleChoices.ADMIN])
|
||||||
def test_api_documents_ask_for_access_accept_authenticated_non_root_document(role):
|
def test_api_documents_ask_for_access_accept_authenticated_non_root_document(role):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ def test_api_documents_children_list_anonymous_public_standalone(
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -63,6 +64,7 @@ def test_api_documents_children_list_anonymous_public_standalone(
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -115,6 +117,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -137,6 +140,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -208,6 +212,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -230,6 +235,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -287,6 +293,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -309,6 +316,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -393,6 +401,7 @@ def test_api_documents_children_list_authenticated_related_direct(
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -415,6 +424,7 @@ def test_api_documents_children_list_authenticated_related_direct(
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -475,6 +485,7 @@ def test_api_documents_children_list_authenticated_related_parent(
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -497,6 +508,7 @@ def test_api_documents_children_list_authenticated_related_parent(
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -609,6 +621,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -631,6 +644,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
|
|||||||
878
src/backend/core/tests/documents/test_api_documents_comments.py
Normal file
@@ -0,0 +1,878 @@
|
|||||||
|
"""Test API for comments on documents."""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from core import factories, models
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
# List comments
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_comments_anonymous_user_public_document():
|
||||||
|
"""Anonymous users should be allowed to list comments on a public document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment1, comment2 = factories.CommentFactory.create_batch(2, thread=thread)
|
||||||
|
# other comments not linked to the document
|
||||||
|
factories.CommentFactory.create_batch(2)
|
||||||
|
|
||||||
|
response = APIClient().get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 2,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": str(comment1.id),
|
||||||
|
"body": comment1.body,
|
||||||
|
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user": {
|
||||||
|
"full_name": comment1.user.full_name,
|
||||||
|
"short_name": comment1.user.short_name,
|
||||||
|
},
|
||||||
|
"abilities": comment1.get_abilities(AnonymousUser()),
|
||||||
|
"reactions": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(comment2.id),
|
||||||
|
"body": comment2.body,
|
||||||
|
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user": {
|
||||||
|
"full_name": comment2.user.full_name,
|
||||||
|
"short_name": comment2.user.short_name,
|
||||||
|
},
|
||||||
|
"abilities": comment2.get_abilities(AnonymousUser()),
|
||||||
|
"reactions": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("link_reach", ["restricted", "authenticated"])
|
||||||
|
def test_list_comments_anonymous_user_non_public_document(link_reach):
|
||||||
|
"""Anonymous users should not be allowed to list comments on a non-public document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach=link_reach, link_role=models.LinkRoleChoices.COMMENTER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
factories.CommentFactory(thread=thread)
|
||||||
|
# other comments not linked to the document
|
||||||
|
factories.CommentFactory.create_batch(2)
|
||||||
|
|
||||||
|
response = APIClient().get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_comments_authenticated_user_accessible_document():
|
||||||
|
"""Authenticated users should be allowed to list comments on an accessible document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment1 = factories.CommentFactory(thread=thread)
|
||||||
|
comment2 = factories.CommentFactory(thread=thread, user=user)
|
||||||
|
# other comments not linked to the document
|
||||||
|
factories.CommentFactory.create_batch(2)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"count": 2,
|
||||||
|
"next": None,
|
||||||
|
"previous": None,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": str(comment1.id),
|
||||||
|
"body": comment1.body,
|
||||||
|
"created_at": comment1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"updated_at": comment1.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user": {
|
||||||
|
"full_name": comment1.user.full_name,
|
||||||
|
"short_name": comment1.user.short_name,
|
||||||
|
},
|
||||||
|
"abilities": comment1.get_abilities(user),
|
||||||
|
"reactions": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(comment2.id),
|
||||||
|
"body": comment2.body,
|
||||||
|
"created_at": comment2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"updated_at": comment2.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user": {
|
||||||
|
"full_name": comment2.user.full_name,
|
||||||
|
"short_name": comment2.user.short_name,
|
||||||
|
},
|
||||||
|
"abilities": comment2.get_abilities(user),
|
||||||
|
"reactions": [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_comments_authenticated_user_non_accessible_document():
|
||||||
|
"""Authenticated users should not be allowed to list comments on a non-accessible document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
factories.CommentFactory(thread=thread)
|
||||||
|
# other comments not linked to the document
|
||||||
|
factories.CommentFactory.create_batch(2)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_comments_authenticated_user_not_enough_access():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to list comments on a document they don't have
|
||||||
|
comment access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
factories.CommentFactory(thread=thread)
|
||||||
|
# other comments not linked to the document
|
||||||
|
factories.CommentFactory.create_batch(2)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
# Create comment
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_comment_anonymous_user_public_document():
|
||||||
|
"""
|
||||||
|
Anonymous users should be allowed to create comments on a public document
|
||||||
|
with commenter link_role.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
client = APIClient()
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
|
||||||
|
{"body": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(response.json()["id"]),
|
||||||
|
"body": "test",
|
||||||
|
"created_at": response.json()["created_at"],
|
||||||
|
"updated_at": response.json()["updated_at"],
|
||||||
|
"user": None,
|
||||||
|
"abilities": {
|
||||||
|
"destroy": False,
|
||||||
|
"update": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"reactions": False,
|
||||||
|
"retrieve": True,
|
||||||
|
},
|
||||||
|
"reactions": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_comment_anonymous_user_non_accessible_document():
|
||||||
|
"""Anonymous users should not be allowed to create comments on a non-accessible document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.READER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
client = APIClient()
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
|
||||||
|
{"body": "test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_comment_authenticated_user_accessible_document():
|
||||||
|
"""Authenticated users should be allowed to create comments on an accessible document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
|
||||||
|
{"body": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(response.json()["id"]),
|
||||||
|
"body": "test",
|
||||||
|
"created_at": response.json()["created_at"],
|
||||||
|
"updated_at": response.json()["updated_at"],
|
||||||
|
"user": {
|
||||||
|
"full_name": user.full_name,
|
||||||
|
"short_name": user.short_name,
|
||||||
|
},
|
||||||
|
"abilities": {
|
||||||
|
"destroy": True,
|
||||||
|
"update": True,
|
||||||
|
"partial_update": True,
|
||||||
|
"reactions": True,
|
||||||
|
"retrieve": True,
|
||||||
|
},
|
||||||
|
"reactions": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_comment_authenticated_user_not_enough_access():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to create comments on a document they don't have
|
||||||
|
comment access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/",
|
||||||
|
{"body": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
# Retrieve comment
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieve_comment_anonymous_user_public_document():
|
||||||
|
"""Anonymous users should be allowed to retrieve comments on a public document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(comment.id),
|
||||||
|
"body": comment.body,
|
||||||
|
"created_at": comment.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"updated_at": comment.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"user": {
|
||||||
|
"full_name": comment.user.full_name,
|
||||||
|
"short_name": comment.user.short_name,
|
||||||
|
},
|
||||||
|
"reactions": [],
|
||||||
|
"abilities": comment.get_abilities(AnonymousUser()),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieve_comment_anonymous_user_non_accessible_document():
|
||||||
|
"""Anonymous users should not be allowed to retrieve comments on a non-accessible document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.READER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieve_comment_authenticated_user_accessible_document():
|
||||||
|
"""Authenticated users should be allowed to retrieve comments on an accessible document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieve_comment_authenticated_user_not_enough_access():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to retrieve comments on a document they don't have
|
||||||
|
comment access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.get(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
# Update comment
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_comment_anonymous_user_public_document():
|
||||||
|
"""Anonymous users should not be allowed to update comments on a public document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test")
|
||||||
|
client = APIClient()
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_comment_anonymous_user_non_accessible_document():
|
||||||
|
"""Anonymous users should not be allowed to update comments on a non-accessible document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.READER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test")
|
||||||
|
client = APIClient()
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_comment_authenticated_user_accessible_document():
|
||||||
|
"""Authenticated users should not be able to update comments not their own."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted",
|
||||||
|
users=[
|
||||||
|
(
|
||||||
|
user,
|
||||||
|
random.choice(
|
||||||
|
[models.LinkRoleChoices.COMMENTER, models.LinkRoleChoices.EDITOR]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test")
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_comment_authenticated_user_own_comment():
|
||||||
|
"""Authenticated users should be able to update comments not their own."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted",
|
||||||
|
users=[
|
||||||
|
(
|
||||||
|
user,
|
||||||
|
random.choice(
|
||||||
|
[models.LinkRoleChoices.COMMENTER, models.LinkRoleChoices.EDITOR]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test", user=user)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
comment.refresh_from_db()
|
||||||
|
assert comment.body == "other content"
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_comment_authenticated_user_not_enough_access():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to update comments on a document they don't
|
||||||
|
have comment access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test")
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_comment_authenticated_no_access():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be allowed to update comments on a document they don't
|
||||||
|
have access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test")
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
|
||||||
|
def test_update_comment_authenticated_admin_or_owner_can_update_any_comment(role):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to update comments on a document they don't have access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(users=[(user, role)])
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test")
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
comment.refresh_from_db()
|
||||||
|
assert comment.body == "other content"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
|
||||||
|
def test_update_comment_authenticated_admin_or_owner_can_update_own_comment(role):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to update comments on a document they don't have access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(users=[(user, role)])
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, body="test", user=user)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/",
|
||||||
|
{"body": "other content"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
comment.refresh_from_db()
|
||||||
|
assert comment.body == "other content"
|
||||||
|
|
||||||
|
|
||||||
|
# Delete comment
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_comment_anonymous_user_public_document():
|
||||||
|
"""Anonymous users should not be allowed to delete comments on a public document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_comment_anonymous_user_non_accessible_document():
|
||||||
|
"""Anonymous users should not be allowed to delete comments on a non-accessible document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.READER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_comment_authenticated_user_accessible_document_own_comment():
|
||||||
|
"""Authenticated users should be able to delete comments on an accessible document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, user=user)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_comment_authenticated_user_accessible_document_not_own_comment():
|
||||||
|
"""Authenticated users should not be able to delete comments on an accessible document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.COMMENTER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
|
||||||
|
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_any_comment(role):
|
||||||
|
"""Authenticated users should be able to delete comments on a document they have access to."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(users=[(user, role)])
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("role", [models.RoleChoices.ADMIN, models.RoleChoices.OWNER])
|
||||||
|
def test_delete_comment_authenticated_user_admin_or_owner_can_delete_own_comment(role):
|
||||||
|
"""Authenticated users should be able to delete comments on a document they have access to."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(users=[(user, role)])
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread, user=user)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_comment_authenticated_user_not_enough_access():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to delete comments on a document they don't
|
||||||
|
have access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.LinkRoleChoices.READER)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/comments/{comment.id!s}/"
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
# Create reaction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("link_role", models.LinkRoleChoices.values)
|
||||||
|
def test_create_reaction_anonymous_user_public_document(link_role):
|
||||||
|
"""No matter the link_role, an anonymous user can not react to a comment."""
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(link_reach="public", link_role=link_role)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_reaction_authenticated_user_public_document():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to reaction to a comment on a public document with
|
||||||
|
link_role reader.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.READER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_reaction_authenticated_user_accessible_public_document():
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to react to a comment on a public document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="public", link_role=models.LinkRoleChoices.COMMENTER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
assert models.Reaction.objects.filter(
|
||||||
|
comment=comment, emoji="test", users__in=[user]
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_reaction_authenticated_user_connected_document_link_role_reader():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to react to a comment on a connected document
|
||||||
|
with link_role reader.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="authenticated", link_role=models.LinkRoleChoices.READER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_role",
|
||||||
|
[
|
||||||
|
role
|
||||||
|
for role in models.LinkRoleChoices.values
|
||||||
|
if role != models.LinkRoleChoices.READER
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_create_reaction_authenticated_user_connected_document(link_role):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to react to a comment on a connected document.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="authenticated", link_role=link_role
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
assert models.Reaction.objects.filter(
|
||||||
|
comment=comment, emoji="test", users__in=[user]
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_reaction_authenticated_user_restricted_accessible_document():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to react to a comment on a restricted accessible document
|
||||||
|
they don't have access to.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted")
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_reaction_authenticated_user_restricted_accessible_document_role_reader():
|
||||||
|
"""
|
||||||
|
Authenticated users should not be able to react to a comment on a restricted accessible
|
||||||
|
document with role reader.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", link_role=models.LinkRoleChoices.READER
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"role",
|
||||||
|
[role for role in models.RoleChoices.values if role != models.RoleChoices.READER],
|
||||||
|
)
|
||||||
|
def test_create_reaction_authenticated_user_restricted_accessible_document_role_commenter(
|
||||||
|
role,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to react to a comment on a restricted accessible document
|
||||||
|
with role commenter.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(link_reach="restricted", users=[(user, role)])
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
assert models.Reaction.objects.filter(
|
||||||
|
comment=comment, emoji="test", users__in=[user]
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": "test"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json() == {"user_already_reacted": True}
|
||||||
|
|
||||||
|
|
||||||
|
# Delete reaction
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_reaction_not_owned_by_the_current_user():
|
||||||
|
"""
|
||||||
|
Users should not be able to delete reactions not owned by the current user.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.RoleChoices.ADMIN)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
reaction = factories.ReactionFactory(comment=comment)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": reaction.emoji},
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_reaction_owned_by_the_current_user():
|
||||||
|
"""
|
||||||
|
Users should not be able to delete reactions not owned by the current user.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_reach="restricted", users=[(user, models.RoleChoices.ADMIN)]
|
||||||
|
)
|
||||||
|
thread = factories.ThreadFactory(document=document)
|
||||||
|
comment = factories.CommentFactory(thread=thread)
|
||||||
|
reaction = factories.ReactionFactory(comment=comment)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
response = client.delete(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/threads/{thread.id!s}/"
|
||||||
|
f"comments/{comment.id!s}/reactions/",
|
||||||
|
{"emoji": reaction.emoji},
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
reaction.refresh_from_db()
|
||||||
|
assert reaction.users.exists()
|
||||||
@@ -38,6 +38,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -62,6 +63,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
|||||||
"computed_link_role": grand_child.computed_link_role,
|
"computed_link_role": grand_child.computed_link_role,
|
||||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_child.creator.id),
|
"creator": str(grand_child.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
"excerpt": grand_child.excerpt,
|
"excerpt": grand_child.excerpt,
|
||||||
"id": str(grand_child.id),
|
"id": str(grand_child.id),
|
||||||
@@ -84,6 +86,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -135,6 +138,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -157,6 +161,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
|||||||
"computed_link_role": grand_child.computed_link_role,
|
"computed_link_role": grand_child.computed_link_role,
|
||||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_child.creator.id),
|
"creator": str(grand_child.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 5,
|
"depth": 5,
|
||||||
"excerpt": grand_child.excerpt,
|
"excerpt": grand_child.excerpt,
|
||||||
"id": str(grand_child.id),
|
"id": str(grand_child.id),
|
||||||
@@ -179,6 +184,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -251,6 +257,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -273,6 +280,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
|||||||
"computed_link_role": grand_child.computed_link_role,
|
"computed_link_role": grand_child.computed_link_role,
|
||||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_child.creator.id),
|
"creator": str(grand_child.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
"excerpt": grand_child.excerpt,
|
"excerpt": grand_child.excerpt,
|
||||||
"id": str(grand_child.id),
|
"id": str(grand_child.id),
|
||||||
@@ -295,6 +303,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -352,6 +361,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -374,6 +384,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
|||||||
"computed_link_role": grand_child.computed_link_role,
|
"computed_link_role": grand_child.computed_link_role,
|
||||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_child.creator.id),
|
"creator": str(grand_child.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 5,
|
"depth": 5,
|
||||||
"excerpt": grand_child.excerpt,
|
"excerpt": grand_child.excerpt,
|
||||||
"id": str(grand_child.id),
|
"id": str(grand_child.id),
|
||||||
@@ -396,6 +407,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -474,6 +486,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -496,6 +509,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
|||||||
"computed_link_role": grand_child.computed_link_role,
|
"computed_link_role": grand_child.computed_link_role,
|
||||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_child.creator.id),
|
"creator": str(grand_child.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
"excerpt": grand_child.excerpt,
|
"excerpt": grand_child.excerpt,
|
||||||
"id": str(grand_child.id),
|
"id": str(grand_child.id),
|
||||||
@@ -518,6 +532,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -576,6 +591,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -598,6 +614,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
|||||||
"computed_link_role": grand_child.computed_link_role,
|
"computed_link_role": grand_child.computed_link_role,
|
||||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_child.creator.id),
|
"creator": str(grand_child.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 5,
|
"depth": 5,
|
||||||
"excerpt": grand_child.excerpt,
|
"excerpt": grand_child.excerpt,
|
||||||
"id": str(grand_child.id),
|
"id": str(grand_child.id),
|
||||||
@@ -620,6 +637,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
@@ -724,6 +742,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
|||||||
"computed_link_role": child1.computed_link_role,
|
"computed_link_role": child1.computed_link_role,
|
||||||
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child1.creator.id),
|
"creator": str(child1.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child1.excerpt,
|
"excerpt": child1.excerpt,
|
||||||
"id": str(child1.id),
|
"id": str(child1.id),
|
||||||
@@ -746,6 +765,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
|||||||
"computed_link_role": grand_child.computed_link_role,
|
"computed_link_role": grand_child.computed_link_role,
|
||||||
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_child.creator.id),
|
"creator": str(grand_child.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
"excerpt": grand_child.excerpt,
|
"excerpt": grand_child.excerpt,
|
||||||
"id": str(grand_child.id),
|
"id": str(grand_child.id),
|
||||||
@@ -768,6 +788,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
|
|||||||
"computed_link_role": child2.computed_link_role,
|
"computed_link_role": child2.computed_link_role,
|
||||||
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(child2.creator.id),
|
"creator": str(child2.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
"excerpt": child2.excerpt,
|
"excerpt": child2.excerpt,
|
||||||
"id": str(child2.id),
|
"id": str(child2.id),
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ def test_api_document_favorite_list_authenticated_with_favorite():
|
|||||||
"computed_link_role": document.computed_link_role,
|
"computed_link_role": document.computed_link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"content": document.content,
|
"content": document.content,
|
||||||
"depth": document.depth,
|
"depth": document.depth,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ def test_api_documents_list_format():
|
|||||||
"computed_link_role": document.computed_link_role,
|
"computed_link_role": document.computed_link_role,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": True,
|
"is_favorite": True,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": document.link_role in ["commenter", "editor"],
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
@@ -46,8 +47,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": False,
|
"mask": False,
|
||||||
@@ -70,6 +71,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
|||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -112,6 +114,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": grand_parent.link_role in ["commenter", "editor"],
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -144,6 +147,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
|||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -218,6 +222,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
|||||||
"children_create": document.link_role == "editor",
|
"children_create": document.link_role == "editor",
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": document.link_role in ["commenter", "editor"],
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -227,8 +232,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": True,
|
"mask": True,
|
||||||
@@ -252,6 +257,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
|||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
"link_reach": reach,
|
"link_reach": reach,
|
||||||
@@ -301,6 +307,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
|||||||
"children_create": grand_parent.link_role == "editor",
|
"children_create": grand_parent.link_role == "editor",
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": grand_parent.link_role in ["commenter", "editor"],
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -333,6 +340,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
|||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
"link_reach": document.link_reach,
|
"link_reach": document.link_reach,
|
||||||
@@ -446,6 +454,7 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
|||||||
"content": document.content,
|
"content": document.content,
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -489,13 +498,14 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
|||||||
"abilities": {
|
"abilities": {
|
||||||
"accesses_manage": access.role in ["administrator", "owner"],
|
"accesses_manage": access.role in ["administrator", "owner"],
|
||||||
"accesses_view": True,
|
"accesses_view": True,
|
||||||
"ai_transform": access.role != "reader",
|
"ai_transform": access.role not in ["reader", "commenter"],
|
||||||
"ai_translate": access.role != "reader",
|
"ai_translate": access.role not in ["reader", "commenter"],
|
||||||
"attachment_upload": access.role != "reader",
|
"attachment_upload": access.role not in ["reader", "commenter"],
|
||||||
"can_edit": access.role != "reader",
|
"can_edit": access.role not in ["reader", "commenter"],
|
||||||
"children_create": access.role != "reader",
|
"children_create": access.role not in ["reader", "commenter"],
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": access.role != "reader",
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -511,11 +521,11 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
|||||||
"media_auth": True,
|
"media_auth": True,
|
||||||
"media_check": True,
|
"media_check": True,
|
||||||
"move": access.role in ["administrator", "owner"],
|
"move": access.role in ["administrator", "owner"],
|
||||||
"partial_update": access.role != "reader",
|
"partial_update": access.role not in ["reader", "commenter"],
|
||||||
"restore": access.role == "owner",
|
"restore": access.role == "owner",
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"tree": True,
|
"tree": True,
|
||||||
"update": access.role != "reader",
|
"update": access.role not in ["reader", "commenter"],
|
||||||
"versions_destroy": access.role in ["administrator", "owner"],
|
"versions_destroy": access.role in ["administrator", "owner"],
|
||||||
"versions_list": True,
|
"versions_list": True,
|
||||||
"versions_retrieve": True,
|
"versions_retrieve": True,
|
||||||
@@ -528,6 +538,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
|||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
"link_reach": "restricted",
|
"link_reach": "restricted",
|
||||||
@@ -683,6 +694,7 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
|||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -749,6 +761,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
|||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -815,6 +828,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
|||||||
"content": document.content,
|
"content": document.content,
|
||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
|
"deleted_at": None,
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
|
|||||||
1226
src/backend/core/tests/documents/test_api_documents_threads.py
Normal file
@@ -48,11 +48,11 @@ def test_api_documents_trashbin_format():
|
|||||||
|
|
||||||
other_users = factories.UserFactory.create_batch(3)
|
other_users = factories.UserFactory.create_batch(3)
|
||||||
document = factories.DocumentFactory(
|
document = factories.DocumentFactory(
|
||||||
deleted_at=timezone.now(),
|
|
||||||
users=factories.UserFactory.create_batch(2),
|
users=factories.UserFactory.create_batch(2),
|
||||||
favorited_by=[user, *other_users],
|
favorited_by=[user, *other_users],
|
||||||
link_traces=other_users,
|
link_traces=other_users,
|
||||||
)
|
)
|
||||||
|
document.soft_delete()
|
||||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||||
|
|
||||||
response = client.get("/api/v1.0/documents/trashbin/")
|
response = client.get("/api/v1.0/documents/trashbin/")
|
||||||
@@ -70,40 +70,41 @@ def test_api_documents_trashbin_format():
|
|||||||
assert results[0] == {
|
assert results[0] == {
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"abilities": {
|
"abilities": {
|
||||||
"accesses_manage": True,
|
"accesses_manage": False,
|
||||||
"accesses_view": True,
|
"accesses_view": False,
|
||||||
"ai_transform": True,
|
"ai_transform": False,
|
||||||
"ai_translate": True,
|
"ai_translate": False,
|
||||||
"attachment_upload": True,
|
"attachment_upload": False,
|
||||||
"can_edit": True,
|
"can_edit": False,
|
||||||
"children_create": True,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": False,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": False,
|
||||||
"descendants": True,
|
"descendants": False,
|
||||||
"cors_proxy": True,
|
"cors_proxy": False,
|
||||||
"content": True,
|
"comment": False,
|
||||||
"destroy": True,
|
"content": False,
|
||||||
"duplicate": True,
|
"destroy": False,
|
||||||
"favorite": True,
|
"duplicate": False,
|
||||||
"invite_owner": True,
|
"favorite": False,
|
||||||
"link_configuration": True,
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": True,
|
"mask": False,
|
||||||
"media_auth": True,
|
"media_auth": False,
|
||||||
"media_check": True,
|
"media_check": False,
|
||||||
"move": False, # Can't move a deleted document
|
"move": False, # Can't move a deleted document
|
||||||
"partial_update": True,
|
"partial_update": False,
|
||||||
"restore": True,
|
"restore": True,
|
||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"tree": True,
|
"tree": True,
|
||||||
"update": True,
|
"update": False,
|
||||||
"versions_destroy": True,
|
"versions_destroy": False,
|
||||||
"versions_list": True,
|
"versions_list": False,
|
||||||
"versions_retrieve": True,
|
"versions_retrieve": False,
|
||||||
},
|
},
|
||||||
"ancestors_link_reach": None,
|
"ancestors_link_reach": None,
|
||||||
"ancestors_link_role": None,
|
"ancestors_link_role": None,
|
||||||
@@ -113,6 +114,7 @@ def test_api_documents_trashbin_format():
|
|||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
|
"deleted_at": document.ancestors_deleted_at.isoformat().replace("+00:00", "Z"),
|
||||||
"link_reach": document.link_reach,
|
"link_reach": document.link_reach,
|
||||||
"link_role": document.link_role,
|
"link_role": document.link_role,
|
||||||
"nb_accesses_ancestors": 0,
|
"nb_accesses_ancestors": 0,
|
||||||
@@ -165,10 +167,10 @@ def test_api_documents_trashbin_authenticated_direct(django_assert_num_queries):
|
|||||||
|
|
||||||
expected_ids = {str(document1.id), str(document2.id), str(document3.id)}
|
expected_ids = {str(document1.id), str(document2.id), str(document3.id)}
|
||||||
|
|
||||||
with django_assert_num_queries(10):
|
with django_assert_num_queries(11):
|
||||||
response = client.get("/api/v1.0/documents/trashbin/")
|
response = client.get("/api/v1.0/documents/trashbin/")
|
||||||
|
|
||||||
with django_assert_num_queries(4):
|
with django_assert_num_queries(5):
|
||||||
response = client.get("/api/v1.0/documents/trashbin/")
|
response = client.get("/api/v1.0/documents/trashbin/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -207,10 +209,10 @@ def test_api_documents_trashbin_authenticated_via_team(
|
|||||||
|
|
||||||
expected_ids = {str(deleted_document_team1.id), str(deleted_document_team2.id)}
|
expected_ids = {str(deleted_document_team1.id), str(deleted_document_team2.id)}
|
||||||
|
|
||||||
with django_assert_num_queries(7):
|
with django_assert_num_queries(8):
|
||||||
response = client.get("/api/v1.0/documents/trashbin/")
|
response = client.get("/api/v1.0/documents/trashbin/")
|
||||||
|
|
||||||
with django_assert_num_queries(3):
|
with django_assert_num_queries(4):
|
||||||
response = client.get("/api/v1.0/documents/trashbin/")
|
response = client.get("/api/v1.0/documents/trashbin/")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -292,3 +294,29 @@ def test_api_documents_trashbin_distinct():
|
|||||||
content = response.json()
|
content = response.json()
|
||||||
assert len(content["results"]) == 1
|
assert len(content["results"]) == 1
|
||||||
assert content["results"][0]["id"] == str(document.id)
|
assert content["results"][0]["id"] == str(document.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_trashbin_empty_queryset_bug():
|
||||||
|
"""
|
||||||
|
Test that users with no owner role don't see documents.
|
||||||
|
"""
|
||||||
|
# Create a new user with no owner access to any document
|
||||||
|
new_user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(new_user)
|
||||||
|
|
||||||
|
# Create some deleted documents owned by other users
|
||||||
|
other_user = factories.UserFactory()
|
||||||
|
item1 = factories.DocumentFactory(users=[(other_user, "owner")])
|
||||||
|
item1.soft_delete()
|
||||||
|
item2 = factories.DocumentFactory(users=[(other_user, "owner")])
|
||||||
|
item2.soft_delete()
|
||||||
|
item3 = factories.DocumentFactory(users=[(other_user, "owner")])
|
||||||
|
item3.soft_delete()
|
||||||
|
|
||||||
|
response = client.get("/api/v1.0/documents/trashbin/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert content["count"] == 0
|
||||||
|
assert len(content["results"]) == 0
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
|||||||
),
|
),
|
||||||
"creator": str(child.creator.id),
|
"creator": str(child.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": child.excerpt,
|
"excerpt": child.excerpt,
|
||||||
"id": str(child.id),
|
"id": str(child.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -73,6 +74,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
|||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -96,6 +98,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
|||||||
"created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(sibling1.creator.id),
|
"creator": str(sibling1.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": sibling1.excerpt,
|
"excerpt": sibling1.excerpt,
|
||||||
"id": str(sibling1.id),
|
"id": str(sibling1.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -119,6 +122,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
|||||||
"created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(sibling2.creator.id),
|
"creator": str(sibling2.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": sibling2.excerpt,
|
"excerpt": sibling2.excerpt,
|
||||||
"id": str(sibling2.id),
|
"id": str(sibling2.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -138,6 +142,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
|
|||||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(parent.creator.id),
|
"creator": str(parent.creator.id),
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent.excerpt,
|
"excerpt": parent.excerpt,
|
||||||
"id": str(parent.id),
|
"id": str(parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -210,6 +215,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(child.creator.id),
|
"creator": str(child.creator.id),
|
||||||
"depth": 5,
|
"depth": 5,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": child.excerpt,
|
"excerpt": child.excerpt,
|
||||||
"id": str(child.id),
|
"id": str(child.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -233,6 +239,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -260,6 +267,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(document_sibling.creator.id),
|
"creator": str(document_sibling.creator.id),
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document_sibling.excerpt,
|
"excerpt": document_sibling.excerpt,
|
||||||
"id": str(document_sibling.id),
|
"id": str(document_sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -281,6 +289,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
|||||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(parent.creator.id),
|
"creator": str(parent.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent.excerpt,
|
"excerpt": parent.excerpt,
|
||||||
"id": str(parent.id),
|
"id": str(parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -306,6 +315,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(parent_sibling.creator.id),
|
"creator": str(parent_sibling.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent_sibling.excerpt,
|
"excerpt": parent_sibling.excerpt,
|
||||||
"id": str(parent_sibling.id),
|
"id": str(parent_sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -327,6 +337,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
|
|||||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_parent.creator.id),
|
"creator": str(grand_parent.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": grand_parent.excerpt,
|
"excerpt": grand_parent.excerpt,
|
||||||
"id": str(grand_parent.id),
|
"id": str(grand_parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -406,6 +417,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
|||||||
),
|
),
|
||||||
"creator": str(child.creator.id),
|
"creator": str(child.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": child.excerpt,
|
"excerpt": child.excerpt,
|
||||||
"id": str(child.id),
|
"id": str(child.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -427,6 +439,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
|||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -450,6 +463,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
|||||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(sibling.creator.id),
|
"creator": str(sibling.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": sibling.excerpt,
|
"excerpt": sibling.excerpt,
|
||||||
"id": str(sibling.id),
|
"id": str(sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -469,6 +483,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
|
|||||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(parent.creator.id),
|
"creator": str(parent.creator.id),
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent.excerpt,
|
"excerpt": parent.excerpt,
|
||||||
"id": str(parent.id),
|
"id": str(parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -546,6 +561,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
|||||||
),
|
),
|
||||||
"creator": str(child.creator.id),
|
"creator": str(child.creator.id),
|
||||||
"depth": 5,
|
"depth": 5,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": child.excerpt,
|
"excerpt": child.excerpt,
|
||||||
"id": str(child.id),
|
"id": str(child.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -569,6 +585,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
|||||||
),
|
),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -596,6 +613,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
|||||||
),
|
),
|
||||||
"creator": str(document_sibling.creator.id),
|
"creator": str(document_sibling.creator.id),
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document_sibling.excerpt,
|
"excerpt": document_sibling.excerpt,
|
||||||
"id": str(document_sibling.id),
|
"id": str(document_sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -617,6 +635,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
|||||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(parent.creator.id),
|
"creator": str(parent.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent.excerpt,
|
"excerpt": parent.excerpt,
|
||||||
"id": str(parent.id),
|
"id": str(parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -642,6 +661,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
|||||||
),
|
),
|
||||||
"creator": str(parent_sibling.creator.id),
|
"creator": str(parent_sibling.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent_sibling.excerpt,
|
"excerpt": parent_sibling.excerpt,
|
||||||
"id": str(parent_sibling.id),
|
"id": str(parent_sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -663,6 +683,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
|
|||||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_parent.creator.id),
|
"creator": str(grand_parent.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": grand_parent.excerpt,
|
"excerpt": grand_parent.excerpt,
|
||||||
"id": str(grand_parent.id),
|
"id": str(grand_parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -744,6 +765,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
|||||||
),
|
),
|
||||||
"creator": str(child.creator.id),
|
"creator": str(child.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": child.excerpt,
|
"excerpt": child.excerpt,
|
||||||
"id": str(child.id),
|
"id": str(child.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -765,6 +787,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
|||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -788,6 +811,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
|||||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(sibling.creator.id),
|
"creator": str(sibling.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": sibling.excerpt,
|
"excerpt": sibling.excerpt,
|
||||||
"id": str(sibling.id),
|
"id": str(sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -807,6 +831,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
|
|||||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(parent.creator.id),
|
"creator": str(parent.creator.id),
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent.excerpt,
|
"excerpt": parent.excerpt,
|
||||||
"id": str(parent.id),
|
"id": str(parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -888,6 +913,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(child.creator.id),
|
"creator": str(child.creator.id),
|
||||||
"depth": 5,
|
"depth": 5,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": child.excerpt,
|
"excerpt": child.excerpt,
|
||||||
"id": str(child.id),
|
"id": str(child.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -911,6 +937,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -938,6 +965,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(document_sibling.creator.id),
|
"creator": str(document_sibling.creator.id),
|
||||||
"depth": 4,
|
"depth": 4,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document_sibling.excerpt,
|
"excerpt": document_sibling.excerpt,
|
||||||
"id": str(document_sibling.id),
|
"id": str(document_sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -959,6 +987,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
|||||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(parent.creator.id),
|
"creator": str(parent.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent.excerpt,
|
"excerpt": parent.excerpt,
|
||||||
"id": str(parent.id),
|
"id": str(parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -984,6 +1013,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
|||||||
),
|
),
|
||||||
"creator": str(parent_sibling.creator.id),
|
"creator": str(parent_sibling.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent_sibling.excerpt,
|
"excerpt": parent_sibling.excerpt,
|
||||||
"id": str(parent_sibling.id),
|
"id": str(parent_sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -1005,6 +1035,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
|
|||||||
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(grand_parent.creator.id),
|
"creator": str(grand_parent.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": grand_parent.excerpt,
|
"excerpt": grand_parent.excerpt,
|
||||||
"id": str(grand_parent.id),
|
"id": str(grand_parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -1094,6 +1125,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
|||||||
),
|
),
|
||||||
"creator": str(child.creator.id),
|
"creator": str(child.creator.id),
|
||||||
"depth": 3,
|
"depth": 3,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": child.excerpt,
|
"excerpt": child.excerpt,
|
||||||
"id": str(child.id),
|
"id": str(child.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -1115,6 +1147,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
|||||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(document.creator.id),
|
"creator": str(document.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": document.excerpt,
|
"excerpt": document.excerpt,
|
||||||
"id": str(document.id),
|
"id": str(document.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -1138,6 +1171,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
|||||||
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(sibling.creator.id),
|
"creator": str(sibling.creator.id),
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": sibling.excerpt,
|
"excerpt": sibling.excerpt,
|
||||||
"id": str(sibling.id),
|
"id": str(sibling.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -1157,6 +1191,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
|||||||
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
|
||||||
"creator": str(parent.creator.id),
|
"creator": str(parent.creator.id),
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
"deleted_at": None,
|
||||||
"excerpt": parent.excerpt,
|
"excerpt": parent.excerpt,
|
||||||
"id": str(parent.id),
|
"id": str(parent.id),
|
||||||
"is_favorite": False,
|
"is_favorite": False,
|
||||||
@@ -1170,3 +1205,56 @@ def test_api_documents_tree_list_authenticated_related_team_members(
|
|||||||
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
|
||||||
"user_role": access.role,
|
"user_role": access.role,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_tree_list_deleted_document():
|
||||||
|
"""
|
||||||
|
Tree of a deleted document should only be accessible to the owner.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
parent = factories.DocumentFactory(link_reach="public")
|
||||||
|
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
|
||||||
|
factories.DocumentFactory(link_reach="public", parent=document)
|
||||||
|
|
||||||
|
document.soft_delete()
|
||||||
|
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_documents_tree_list_deleted_document_owner(django_assert_num_queries):
|
||||||
|
"""
|
||||||
|
Tree of a deleted document should only be accessible to the owner.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
parent = factories.DocumentFactory(link_reach="public", users=[(user, "owner")])
|
||||||
|
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
|
||||||
|
child = factories.DocumentFactory(parent=document)
|
||||||
|
|
||||||
|
document.soft_delete()
|
||||||
|
document.refresh_from_db()
|
||||||
|
child.refresh_from_db()
|
||||||
|
|
||||||
|
with django_assert_num_queries(9):
|
||||||
|
client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||||
|
|
||||||
|
with django_assert_num_queries(5):
|
||||||
|
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
content = response.json()
|
||||||
|
assert content["id"] == str(document.id)
|
||||||
|
assert content["deleted_at"] == document.deleted_at.isoformat().replace(
|
||||||
|
"+00:00", "Z"
|
||||||
|
)
|
||||||
|
assert len(content["children"]) == 1
|
||||||
|
assert content["children"][0]["id"] == str(child.id)
|
||||||
|
assert content["children"][0][
|
||||||
|
"deleted_at"
|
||||||
|
] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z")
|
||||||
|
|||||||
@@ -1,799 +0,0 @@
|
|||||||
"""
|
|
||||||
Test template accesses API endpoints for users in impress's core app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
from unittest import mock
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from rest_framework.test import APIClient
|
|
||||||
|
|
||||||
from core import factories, models
|
|
||||||
from core.api import serializers
|
|
||||||
from core.tests.conftest import TEAM, USER, VIA
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_list_anonymous():
|
|
||||||
"""Anonymous users should not be allowed to list template accesses."""
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
factories.UserTemplateAccessFactory.create_batch(2, template=template)
|
|
||||||
|
|
||||||
response = APIClient().get(f"/api/v1.0/templates/{template.id!s}/accesses/")
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "Authentication credentials were not provided."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_list_authenticated_unrelated():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to list template accesses for a template
|
|
||||||
to which they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
factories.UserTemplateAccessFactory.create_batch(3, template=template)
|
|
||||||
|
|
||||||
# Accesses for other templates to which the user is related should not be listed either
|
|
||||||
other_access = factories.UserTemplateAccessFactory(user=user)
|
|
||||||
factories.UserTemplateAccessFactory(template=other_access.template)
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json() == []
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_list_authenticated_related(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Authenticated users should be able to list template accesses for a template
|
|
||||||
to which they are directly related, whatever their role in the template.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
user_access = None
|
|
||||||
if via == USER:
|
|
||||||
user_access = models.TemplateAccess.objects.create(
|
|
||||||
template=template,
|
|
||||||
user=user,
|
|
||||||
role=random.choice(models.RoleChoices.values),
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
user_access = models.TemplateAccess.objects.create(
|
|
||||||
template=template,
|
|
||||||
team="lasuite",
|
|
||||||
role=random.choice(models.RoleChoices.values),
|
|
||||||
)
|
|
||||||
|
|
||||||
access1 = factories.TeamTemplateAccessFactory(template=template)
|
|
||||||
access2 = factories.UserTemplateAccessFactory(template=template)
|
|
||||||
|
|
||||||
# Accesses for other templates to which the user is related should not be listed either
|
|
||||||
other_access = factories.UserTemplateAccessFactory(user=user)
|
|
||||||
factories.UserTemplateAccessFactory(template=other_access.template)
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
content = response.json()
|
|
||||||
assert len(content) == 3
|
|
||||||
assert sorted(content, key=lambda x: x["id"]) == sorted(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": str(user_access.id),
|
|
||||||
"user": str(user.id) 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": str(access2.user.id),
|
|
||||||
"team": "",
|
|
||||||
"role": access2.role,
|
|
||||||
"abilities": access2.get_abilities(user),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
key=lambda x: x["id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_retrieve_anonymous():
|
|
||||||
"""
|
|
||||||
Anonymous users should not be allowed to retrieve a template access.
|
|
||||||
"""
|
|
||||||
access = factories.UserTemplateAccessFactory()
|
|
||||||
|
|
||||||
response = APIClient().get(
|
|
||||||
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "Authentication credentials were not provided."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_retrieve_authenticated_unrelated():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to retrieve a template access for
|
|
||||||
a template to which they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
access = factories.UserTemplateAccessFactory(template=template)
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "You do not have permission to perform this action."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Accesses related to another template should be excluded even if the user is related to it
|
|
||||||
for access in [
|
|
||||||
factories.UserTemplateAccessFactory(),
|
|
||||||
factories.UserTemplateAccessFactory(user=user),
|
|
||||||
]:
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "No TemplateAccess matches the given query."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_retrieve_authenticated_related(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
A user who is related to a template should be allowed to retrieve the
|
|
||||||
associated template user accesses.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(template=template, team="lasuite")
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory(template=template)
|
|
||||||
|
|
||||||
response = client.get(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json() == {
|
|
||||||
"id": str(access.id),
|
|
||||||
"user": str(access.user.id),
|
|
||||||
"team": "",
|
|
||||||
"role": access.role,
|
|
||||||
"abilities": access.get_abilities(user),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_update_anonymous():
|
|
||||||
"""Anonymous users should not be allowed to update a template access."""
|
|
||||||
access = factories.UserTemplateAccessFactory()
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
|
|
||||||
new_values = {
|
|
||||||
"id": uuid4(),
|
|
||||||
"user": factories.UserFactory().id,
|
|
||||||
"role": random.choice(models.RoleChoices.values),
|
|
||||||
}
|
|
||||||
|
|
||||||
api_client = APIClient()
|
|
||||||
for field, value in new_values.items():
|
|
||||||
response = api_client.put(
|
|
||||||
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
|
||||||
{**old_values, field: value},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
assert response.status_code == 401
|
|
||||||
|
|
||||||
access.refresh_from_db()
|
|
||||||
updated_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
assert updated_values == old_values
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_update_authenticated_unrelated():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to update a template access for a template to which
|
|
||||||
they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory()
|
|
||||||
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
new_values = {
|
|
||||||
"id": uuid4(),
|
|
||||||
"user": factories.UserFactory().id,
|
|
||||||
"role": random.choice(models.RoleChoices.values),
|
|
||||||
}
|
|
||||||
|
|
||||||
for field, value in new_values.items():
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
|
||||||
{**old_values, field: value},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
access.refresh_from_db()
|
|
||||||
updated_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
assert updated_values == old_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_update_authenticated_editor_or_reader(
|
|
||||||
via, role, mock_user_teams
|
|
||||||
):
|
|
||||||
"""Editors or readers of a template should not be allowed to update its accesses."""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role=role
|
|
||||||
)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory(template=template)
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
|
|
||||||
new_values = {
|
|
||||||
"id": uuid4(),
|
|
||||||
"user": factories.UserFactory().id,
|
|
||||||
"role": random.choice(models.RoleChoices.values),
|
|
||||||
}
|
|
||||||
|
|
||||||
for field, value in new_values.items():
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
|
||||||
{**old_values, field: value},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
access.refresh_from_db()
|
|
||||||
updated_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
assert updated_values == old_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_update_administrator_except_owner(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
A user who is a direct administrator in a template should be allowed to update a user
|
|
||||||
access for this template, as long as they don't try to set the role to owner.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="administrator"
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="administrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory(
|
|
||||||
template=template,
|
|
||||||
role=random.choice(["administrator", "editor", "reader"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
new_values = {
|
|
||||||
"id": uuid4(),
|
|
||||||
"user_id": factories.UserFactory().id,
|
|
||||||
"role": random.choice(["administrator", "editor", "reader"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
for field, value in new_values.items():
|
|
||||||
new_data = {**old_values, field: value}
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
data=new_data,
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
new_data["role"] == old_values["role"]
|
|
||||||
): # we are not really updating the role
|
|
||||||
assert response.status_code == 403
|
|
||||||
else:
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
access.refresh_from_db()
|
|
||||||
updated_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
if field == "role":
|
|
||||||
assert updated_values == {**old_values, "role": new_values["role"]}
|
|
||||||
else:
|
|
||||||
assert updated_values == old_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_update_administrator_from_owner(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
A user who is an administrator in a template, should not be allowed to update
|
|
||||||
the user access of an "owner" for this template.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="administrator"
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="administrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
other_user = factories.UserFactory()
|
|
||||||
access = factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=other_user, role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
new_values = {
|
|
||||||
"id": uuid4(),
|
|
||||||
"user_id": factories.UserFactory().id,
|
|
||||||
"role": random.choice(models.RoleChoices.values),
|
|
||||||
}
|
|
||||||
|
|
||||||
for field, value in new_values.items():
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
data={**old_values, field: value},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
access.refresh_from_db()
|
|
||||||
updated_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
assert updated_values == old_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_update_administrator_to_owner(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
A user who is an administrator in a template, should not be allowed to update
|
|
||||||
the user access of another user to grant template ownership.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="administrator"
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="administrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
other_user = factories.UserFactory()
|
|
||||||
access = factories.UserTemplateAccessFactory(
|
|
||||||
template=template,
|
|
||||||
user=other_user,
|
|
||||||
role=random.choice(["administrator", "editor", "reader"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
new_values = {
|
|
||||||
"id": uuid4(),
|
|
||||||
"user_id": factories.UserFactory().id,
|
|
||||||
"role": "owner",
|
|
||||||
}
|
|
||||||
|
|
||||||
for field, value in new_values.items():
|
|
||||||
new_data = {**old_values, field: value}
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
data=new_data,
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
# We are not allowed or not really updating the role
|
|
||||||
if field == "role" or new_data["role"] == old_values["role"]:
|
|
||||||
assert response.status_code == 403
|
|
||||||
else:
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
access.refresh_from_db()
|
|
||||||
updated_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
assert updated_values == old_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_update_owner(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
A user who is an owner in a template should be allowed to update
|
|
||||||
a user access for this template whatever the role.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
factories.UserFactory()
|
|
||||||
access = factories.UserTemplateAccessFactory(
|
|
||||||
template=template,
|
|
||||||
)
|
|
||||||
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
new_values = {
|
|
||||||
"id": uuid4(),
|
|
||||||
"user_id": factories.UserFactory().id,
|
|
||||||
"role": random.choice(models.RoleChoices.values),
|
|
||||||
}
|
|
||||||
|
|
||||||
for field, value in new_values.items():
|
|
||||||
new_data = {**old_values, field: value}
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
data=new_data,
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
new_data["role"] == old_values["role"]
|
|
||||||
): # we are not really updating the role
|
|
||||||
assert response.status_code == 403
|
|
||||||
else:
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
access.refresh_from_db()
|
|
||||||
updated_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
|
|
||||||
if field == "role":
|
|
||||||
assert updated_values == {**old_values, "role": new_values["role"]}
|
|
||||||
else:
|
|
||||||
assert updated_values == old_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_update_owner_self(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
A user who is owner of a template should be allowed to update
|
|
||||||
their own user access provided there are other owners in the template.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
access = factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="owner"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
access = factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
|
||||||
new_role = random.choice(["administrator", "editor", "reader"])
|
|
||||||
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
data={**old_values, "role": new_role},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
access.refresh_from_db()
|
|
||||||
assert access.role == "owner"
|
|
||||||
|
|
||||||
# Add another owner and it should now work
|
|
||||||
factories.UserTemplateAccessFactory(template=template, role="owner")
|
|
||||||
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.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
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_delete_anonymous():
|
|
||||||
"""Anonymous users should not be allowed to destroy a template access."""
|
|
||||||
access = factories.UserTemplateAccessFactory()
|
|
||||||
|
|
||||||
response = APIClient().delete(
|
|
||||||
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert models.TemplateAccess.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_delete_authenticated():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to delete a template access for a
|
|
||||||
template to which they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory()
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{access.template_id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert models.TemplateAccess.objects.count() == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to delete a template access for a
|
|
||||||
template in which they are a simple editor or reader.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role=role
|
|
||||||
)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory(template=template)
|
|
||||||
|
|
||||||
assert models.TemplateAccess.objects.count() == 3
|
|
||||||
assert models.TemplateAccess.objects.filter(user=access.user).exists()
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert models.TemplateAccess.objects.count() == 3
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_delete_administrators_except_owners(
|
|
||||||
via, mock_user_teams
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Users who are administrators in a template should be allowed to delete an access
|
|
||||||
from the template provided it is not ownership.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="administrator"
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="administrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory(
|
|
||||||
template=template, role=random.choice(["reader", "editor", "administrator"])
|
|
||||||
)
|
|
||||||
|
|
||||||
assert models.TemplateAccess.objects.count() == 2
|
|
||||||
assert models.TemplateAccess.objects.filter(user=access.user).exists()
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 204
|
|
||||||
assert models.TemplateAccess.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_delete_administrator_on_owners(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Users who are administrators in a template should not be allowed to delete an ownership
|
|
||||||
access from the template.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="administrator"
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="administrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory(template=template, role="owner")
|
|
||||||
|
|
||||||
assert models.TemplateAccess.objects.count() == 3
|
|
||||||
assert models.TemplateAccess.objects.filter(user=access.user).exists()
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert models.TemplateAccess.objects.count() == 3
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_delete_owners(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Users should be able to delete the template access of another user
|
|
||||||
for a template of which they are owner.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
access = factories.UserTemplateAccessFactory(template=template)
|
|
||||||
|
|
||||||
assert models.TemplateAccess.objects.count() == 2
|
|
||||||
assert models.TemplateAccess.objects.filter(user=access.user).exists()
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 204
|
|
||||||
assert models.TemplateAccess.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_delete_owners_last_owner(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
It should not be possible to delete the last owner access from a template
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
access = None
|
|
||||||
if via == USER:
|
|
||||||
access = factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="owner"
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
access = factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert models.TemplateAccess.objects.count() == 2
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert models.TemplateAccess.objects.count() == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_throttling(settings):
|
|
||||||
"""Test api template accesses throttling."""
|
|
||||||
current_rate = settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"]
|
|
||||||
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = "2/minute"
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
user = factories.UserFactory()
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="administrator"
|
|
||||||
)
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
for _i in range(2):
|
|
||||||
response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/")
|
|
||||||
assert response.status_code == 200
|
|
||||||
with mock.patch("core.api.throttling.capture_message") as mock_capture_message:
|
|
||||||
response = client.get(f"/api/v1.0/templates/{template.id!s}/accesses/")
|
|
||||||
assert response.status_code == 429
|
|
||||||
mock_capture_message.assert_called_once_with(
|
|
||||||
"Rate limit exceeded for scope template_access", "warning"
|
|
||||||
)
|
|
||||||
settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["template_access"] = current_rate
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
"""
|
|
||||||
Test template accesses create API endpoint for users in impress's core app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from rest_framework.test import APIClient
|
|
||||||
|
|
||||||
from core import factories, models
|
|
||||||
from core.tests.conftest import TEAM, USER, VIA
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_create_anonymous():
|
|
||||||
"""Anonymous users should not be allowed to create template accesses."""
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
|
|
||||||
other_user = factories.UserFactory()
|
|
||||||
response = APIClient().post(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
{
|
|
||||||
"user": str(other_user.id),
|
|
||||||
"template": str(template.id),
|
|
||||||
"role": random.choice(models.RoleChoices.values),
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "Authentication credentials were not provided."
|
|
||||||
}
|
|
||||||
assert models.TemplateAccess.objects.exists() is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_template_accesses_create_authenticated_unrelated():
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to create template accesses for a template to
|
|
||||||
which they are not related.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
other_user = factories.UserFactory()
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
{
|
|
||||||
"user": str(other_user.id),
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert not models.TemplateAccess.objects.filter(user=other_user).exists()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_create_authenticated_editor_or_reader(
|
|
||||||
via, role, mock_user_teams
|
|
||||||
):
|
|
||||||
"""Editors or readers of a template should not be allowed to create template accesses."""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role=role
|
|
||||||
)
|
|
||||||
|
|
||||||
other_user = factories.UserFactory()
|
|
||||||
|
|
||||||
for new_role in [role[0] for role in models.RoleChoices.choices]:
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
{
|
|
||||||
"user": str(other_user.id),
|
|
||||||
"role": new_role,
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
assert not models.TemplateAccess.objects.filter(user=other_user).exists()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_create_authenticated_administrator(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Administrators of a template should be able to create template accesses
|
|
||||||
except for the "owner" role.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role="administrator"
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="administrator"
|
|
||||||
)
|
|
||||||
|
|
||||||
other_user = factories.UserFactory()
|
|
||||||
|
|
||||||
# It should not be allowed to create an owner access
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
{
|
|
||||||
"user": str(other_user.id),
|
|
||||||
"role": "owner",
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "Only owners of a template 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"]
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
{
|
|
||||||
"user": str(other_user.id),
|
|
||||||
"role": role,
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert models.TemplateAccess.objects.filter(user=other_user).count() == 1
|
|
||||||
new_template_access = models.TemplateAccess.objects.filter(user=other_user).get()
|
|
||||||
assert response.json() == {
|
|
||||||
"abilities": new_template_access.get_abilities(user),
|
|
||||||
"id": str(new_template_access.id),
|
|
||||||
"team": "",
|
|
||||||
"role": role,
|
|
||||||
"user": str(other_user.id),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_template_accesses_create_authenticated_owner(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Owners of a template should be able to create template accesses whatever the role.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory(with_owned_template=True)
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
other_user = factories.UserFactory()
|
|
||||||
|
|
||||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
|
||||||
{
|
|
||||||
"user": str(other_user.id),
|
|
||||||
"role": role,
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
assert models.TemplateAccess.objects.filter(user=other_user).count() == 1
|
|
||||||
new_template_access = models.TemplateAccess.objects.filter(user=other_user).get()
|
|
||||||
assert response.json() == {
|
|
||||||
"id": str(new_template_access.id),
|
|
||||||
"user": str(other_user.id),
|
|
||||||
"team": "",
|
|
||||||
"role": role,
|
|
||||||
"abilities": new_template_access.get_abilities(user),
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,5 @@ def test_api_templates_create_authenticated():
|
|||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 405
|
||||||
template = Template.objects.get()
|
assert not Template.objects.exists()
|
||||||
assert template.title == "my template"
|
|
||||||
assert template.accesses.filter(role="owner", user=user).exists()
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import pytest
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core import factories, models
|
from core import factories, models
|
||||||
from core.tests.conftest import TEAM, USER, VIA
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ def test_api_templates_delete_anonymous():
|
|||||||
assert models.Template.objects.count() == 1
|
assert models.Template.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
def test_api_templates_delete_authenticated_unrelated():
|
def test_api_templates_delete_not_implemented():
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be allowed to delete a template to which they are not
|
Authenticated users should not be allowed to delete a template to which they are not
|
||||||
related.
|
related.
|
||||||
@@ -36,72 +35,11 @@ def test_api_templates_delete_authenticated_unrelated():
|
|||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
is_public = random.choice([True, False])
|
is_public = random.choice([True, False])
|
||||||
template = factories.TemplateFactory(is_public=is_public)
|
template = factories.TemplateFactory(is_public=is_public, users=[(user, "owner")])
|
||||||
|
|
||||||
response = client.delete(
|
response = client.delete(
|
||||||
f"/api/v1.0/templates/{template.id!s}/",
|
f"/api/v1.0/templates/{template.id!s}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403 if is_public else 404
|
assert response.status_code == 405
|
||||||
assert models.Template.objects.count() == 1
|
assert models.Template.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role", ["reader", "editor", "administrator"])
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_templates_delete_authenticated_member_or_administrator(
|
|
||||||
via, role, mock_user_teams
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Authenticated users should not be allowed to delete a template for which they are
|
|
||||||
only a member or administrator.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role=role
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{template.id}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "You do not have permission to perform this action."
|
|
||||||
}
|
|
||||||
assert models.Template.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_templates_delete_authenticated_owner(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Authenticated users should be able to delete a template they own.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.delete(
|
|
||||||
f"/api/v1.0/templates/{template.id}/",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 204
|
|
||||||
assert models.Template.objects.exists() is False
|
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
Tests for Templates API endpoint in impress's core app: update
|
Tests for Templates API endpoint in impress's core app: update
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core import factories
|
from core import factories
|
||||||
from core.api import serializers
|
from core.api import serializers
|
||||||
from core.tests.conftest import TEAM, USER, VIA
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
@@ -17,7 +14,6 @@ pytestmark = pytest.mark.django_db
|
|||||||
def test_api_templates_update_anonymous():
|
def test_api_templates_update_anonymous():
|
||||||
"""Anonymous users should not be allowed to update a template."""
|
"""Anonymous users should not be allowed to update a template."""
|
||||||
template = factories.TemplateFactory()
|
template = factories.TemplateFactory()
|
||||||
old_template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
|
|
||||||
new_template_values = serializers.TemplateSerializer(
|
new_template_values = serializers.TemplateSerializer(
|
||||||
instance=factories.TemplateFactory()
|
instance=factories.TemplateFactory()
|
||||||
@@ -28,145 +24,18 @@ def test_api_templates_update_anonymous():
|
|||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
assert response.json() == {
|
|
||||||
"detail": "Authentication credentials were not provided."
|
|
||||||
}
|
|
||||||
|
|
||||||
template.refresh_from_db()
|
|
||||||
template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
assert template_values == old_template_values
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_templates_update_authenticated_unrelated():
|
def test_api_templates_update_not_implemented():
|
||||||
"""
|
"""
|
||||||
Authenticated users should not be allowed to update a template to which they are not related.
|
Authenticated users should not be allowed to update a template.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
template = factories.TemplateFactory(is_public=False)
|
template = factories.TemplateFactory(users=[(user, "owner")])
|
||||||
old_template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
|
|
||||||
new_template_values = serializers.TemplateSerializer(
|
|
||||||
instance=factories.TemplateFactory()
|
|
||||||
).data
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/",
|
|
||||||
new_template_values,
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "You do not have permission to perform this action."
|
|
||||||
}
|
|
||||||
|
|
||||||
template.refresh_from_db()
|
|
||||||
template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
assert template_values == old_template_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_templates_update_authenticated_readers(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Users who are readers of a template should not be allowed to update it.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role="reader")
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="reader"
|
|
||||||
)
|
|
||||||
|
|
||||||
old_template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
|
|
||||||
new_template_values = serializers.TemplateSerializer(
|
|
||||||
instance=factories.TemplateFactory()
|
|
||||||
).data
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/",
|
|
||||||
new_template_values,
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
assert response.json() == {
|
|
||||||
"detail": "You do not have permission to perform this action."
|
|
||||||
}
|
|
||||||
|
|
||||||
template.refresh_from_db()
|
|
||||||
template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
assert template_values == old_template_values
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_templates_update_authenticated_editor_or_administrator_or_owner(
|
|
||||||
via, role, mock_user_teams
|
|
||||||
):
|
|
||||||
"""Administrator or owner of a template should be allowed to update it."""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role=role
|
|
||||||
)
|
|
||||||
|
|
||||||
old_template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
|
|
||||||
new_template_values = serializers.TemplateSerializer(
|
|
||||||
instance=factories.TemplateFactory()
|
|
||||||
).data
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/",
|
|
||||||
new_template_values,
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
template.refresh_from_db()
|
|
||||||
template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
for key, value in template_values.items():
|
|
||||||
if key in ["id", "accesses"]:
|
|
||||||
assert value == old_template_values[key]
|
|
||||||
else:
|
|
||||||
assert value == new_template_values[key]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
|
||||||
def test_api_templates_update_authenticated_owners(via, mock_user_teams):
|
|
||||||
"""Administrators of a template should be allowed to update it."""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(template=template, user=user, role="owner")
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template, team="lasuite", role="owner"
|
|
||||||
)
|
|
||||||
|
|
||||||
old_template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
|
|
||||||
new_template_values = serializers.TemplateSerializer(
|
new_template_values = serializers.TemplateSerializer(
|
||||||
instance=factories.TemplateFactory()
|
instance=factories.TemplateFactory()
|
||||||
@@ -176,55 +45,10 @@ def test_api_templates_update_authenticated_owners(via, mock_user_teams):
|
|||||||
f"/api/v1.0/templates/{template.id!s}/", new_template_values, format="json"
|
f"/api/v1.0/templates/{template.id!s}/", new_template_values, format="json"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 405
|
||||||
template.refresh_from_db()
|
|
||||||
template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
for key, value in template_values.items():
|
|
||||||
if key in ["id", "accesses"]:
|
|
||||||
assert value == old_template_values[key]
|
|
||||||
else:
|
|
||||||
assert value == new_template_values[key]
|
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
@pytest.mark.parametrize("via", VIA)
|
f"/api/v1.0/templates/{template.id!s}/", new_template_values, format="json"
|
||||||
def test_api_templates_update_administrator_or_owner_of_another(via, mock_user_teams):
|
|
||||||
"""
|
|
||||||
Being administrator or owner of a template should not grant authorization to update
|
|
||||||
another template.
|
|
||||||
"""
|
|
||||||
user = factories.UserFactory()
|
|
||||||
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
template = factories.TemplateFactory()
|
|
||||||
if via == USER:
|
|
||||||
factories.UserTemplateAccessFactory(
|
|
||||||
template=template, user=user, role=random.choice(["administrator", "owner"])
|
|
||||||
)
|
|
||||||
elif via == TEAM:
|
|
||||||
mock_user_teams.return_value = ["lasuite", "unknown"]
|
|
||||||
factories.TeamTemplateAccessFactory(
|
|
||||||
template=template,
|
|
||||||
team="lasuite",
|
|
||||||
role=random.choice(["administrator", "owner"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
is_public = random.choice([True, False])
|
|
||||||
template = factories.TemplateFactory(title="Old title", is_public=is_public)
|
|
||||||
old_template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
|
|
||||||
new_template_values = serializers.TemplateSerializer(
|
|
||||||
instance=factories.TemplateFactory()
|
|
||||||
).data
|
|
||||||
response = client.put(
|
|
||||||
f"/api/v1.0/templates/{template.id!s}/",
|
|
||||||
new_template_values,
|
|
||||||
format="json",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403 if is_public else 404
|
assert response.status_code == 405
|
||||||
|
|
||||||
template.refresh_from_db()
|
|
||||||
template_values = serializers.TemplateSerializer(instance=template).data
|
|
||||||
assert template_values == old_template_values
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ def test_api_config(is_authenticated):
|
|||||||
response = client.get("/api/v1.0/config/")
|
response = client.get("/api/v1.0/config/")
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
|
"AI_FEATURE_ENABLED": False,
|
||||||
"COLLABORATION_WS_URL": "http://testcollab/",
|
"COLLABORATION_WS_URL": "http://testcollab/",
|
||||||
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
|
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
|
||||||
"CRISP_WEBSITE_ID": "123",
|
"CRISP_WEBSITE_ID": "123",
|
||||||
@@ -60,7 +61,7 @@ def test_api_config(is_authenticated):
|
|||||||
"MEDIA_BASE_URL": "http://testserver/",
|
"MEDIA_BASE_URL": "http://testserver/",
|
||||||
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
|
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
|
||||||
"SENTRY_DSN": "https://sentry.test/123",
|
"SENTRY_DSN": "https://sentry.test/123",
|
||||||
"AI_FEATURE_ENABLED": False,
|
"TRASHBIN_CUTOFF_DAYS": 30,
|
||||||
"theme_customization": {},
|
"theme_customization": {},
|
||||||
}
|
}
|
||||||
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
|
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
|
||||||
|
|||||||
@@ -278,6 +278,35 @@ def test_api_users_retrieve_me_authenticated():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_users_retrieve_me_authenticated_empty_name():
|
||||||
|
"""
|
||||||
|
Authenticated users should be able to retrieve their own user via the "/users/me" path.
|
||||||
|
when no name is provided, the full name and short name should be the email without the domain.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(
|
||||||
|
email="test_foo@test.com",
|
||||||
|
full_name=None,
|
||||||
|
short_name=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
factories.UserFactory.create_batch(2)
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/users/me/",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"id": str(user.id),
|
||||||
|
"email": "test_foo@test.com",
|
||||||
|
"full_name": "test_foo",
|
||||||
|
"language": user.language,
|
||||||
|
"short_name": "test_foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_users_retrieve_anonymous():
|
def test_api_users_retrieve_anonymous():
|
||||||
"""Anonymous users should not be allowed to retrieve a user."""
|
"""Anonymous users should not be allowed to retrieve a user."""
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
|
|||||||
283
src/backend/core/tests/test_models_comment.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
"""Test the comment model."""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core import factories
|
||||||
|
from core.models import LinkReachChoices, LinkRoleChoices, RoleChoices
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"role,can_comment",
|
||||||
|
[
|
||||||
|
(LinkRoleChoices.READER, False),
|
||||||
|
(LinkRoleChoices.COMMENTER, True),
|
||||||
|
(LinkRoleChoices.EDITOR, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_anonymous_user_public_document(role, can_comment):
|
||||||
|
"""Anonymous users cannot comment on a document."""
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_role=role, link_reach=LinkReachChoices.PUBLIC
|
||||||
|
)
|
||||||
|
comment = factories.CommentFactory(thread__document=document)
|
||||||
|
user = AnonymousUser()
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": False,
|
||||||
|
"update": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"reactions": False,
|
||||||
|
"retrieve": can_comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_reach", [LinkReachChoices.RESTRICTED, LinkReachChoices.AUTHENTICATED]
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_anonymous_user_restricted_document(link_reach):
|
||||||
|
"""Anonymous users cannot comment on a restricted document."""
|
||||||
|
document = factories.DocumentFactory(link_reach=link_reach)
|
||||||
|
comment = factories.CommentFactory(thread__document=document)
|
||||||
|
user = AnonymousUser()
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": False,
|
||||||
|
"update": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"reactions": False,
|
||||||
|
"retrieve": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_role,link_reach,can_comment",
|
||||||
|
[
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC, False),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.PUBLIC, True),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC, True),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED, False),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.RESTRICTED, False),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED, False),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED, False),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.AUTHENTICATED, True),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_user_reader(link_role, link_reach, can_comment):
|
||||||
|
"""Readers cannot comment on a document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.READER)]
|
||||||
|
)
|
||||||
|
comment = factories.CommentFactory(thread__document=document)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": False,
|
||||||
|
"update": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"reactions": can_comment,
|
||||||
|
"retrieve": can_comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_role,link_reach,can_comment",
|
||||||
|
[
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC, False),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.PUBLIC, True),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC, True),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED, False),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.RESTRICTED, False),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED, False),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED, False),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.AUTHENTICATED, True),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_user_reader_own_comment(
|
||||||
|
link_role, link_reach, can_comment
|
||||||
|
):
|
||||||
|
"""User with reader role on a document has all accesses to its own comment."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.READER)]
|
||||||
|
)
|
||||||
|
comment = factories.CommentFactory(
|
||||||
|
thread__document=document, user=user if can_comment else None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": can_comment,
|
||||||
|
"update": can_comment,
|
||||||
|
"partial_update": can_comment,
|
||||||
|
"reactions": can_comment,
|
||||||
|
"retrieve": can_comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_role,link_reach",
|
||||||
|
[
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_user_commenter(link_role, link_reach):
|
||||||
|
"""Commenters can comment on a document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_role=link_role,
|
||||||
|
link_reach=link_reach,
|
||||||
|
users=[(user, RoleChoices.COMMENTER)],
|
||||||
|
)
|
||||||
|
comment = factories.CommentFactory(thread__document=document)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": False,
|
||||||
|
"update": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"reactions": True,
|
||||||
|
"retrieve": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_role,link_reach",
|
||||||
|
[
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_user_commenter_own_comment(link_role, link_reach):
|
||||||
|
"""Commenters have all accesses to its own comment."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_role=link_role,
|
||||||
|
link_reach=link_reach,
|
||||||
|
users=[(user, RoleChoices.COMMENTER)],
|
||||||
|
)
|
||||||
|
comment = factories.CommentFactory(thread__document=document, user=user)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": True,
|
||||||
|
"update": True,
|
||||||
|
"partial_update": True,
|
||||||
|
"reactions": True,
|
||||||
|
"retrieve": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_role,link_reach",
|
||||||
|
[
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_user_editor(link_role, link_reach):
|
||||||
|
"""Editors can comment on a document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.EDITOR)]
|
||||||
|
)
|
||||||
|
comment = factories.CommentFactory(thread__document=document)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": False,
|
||||||
|
"update": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"reactions": True,
|
||||||
|
"retrieve": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link_role,link_reach",
|
||||||
|
[
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.PUBLIC),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.RESTRICTED),
|
||||||
|
(LinkRoleChoices.READER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.COMMENTER, LinkReachChoices.AUTHENTICATED),
|
||||||
|
(LinkRoleChoices.EDITOR, LinkReachChoices.AUTHENTICATED),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_comment_get_abilities_user_editor_own_comment(link_role, link_reach):
|
||||||
|
"""Editors have all accesses to its own comment."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(
|
||||||
|
link_role=link_role, link_reach=link_reach, users=[(user, RoleChoices.EDITOR)]
|
||||||
|
)
|
||||||
|
comment = factories.CommentFactory(thread__document=document, user=user)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": True,
|
||||||
|
"update": True,
|
||||||
|
"partial_update": True,
|
||||||
|
"reactions": True,
|
||||||
|
"retrieve": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_get_abilities_user_admin():
|
||||||
|
"""Admins have all accesses to a comment."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(users=[(user, RoleChoices.ADMIN)])
|
||||||
|
comment = factories.CommentFactory(
|
||||||
|
thread__document=document, user=random.choice([user, None])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": True,
|
||||||
|
"update": True,
|
||||||
|
"partial_update": True,
|
||||||
|
"reactions": True,
|
||||||
|
"retrieve": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_get_abilities_user_owner():
|
||||||
|
"""Owners have all accesses to a comment."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(users=[(user, RoleChoices.OWNER)])
|
||||||
|
comment = factories.CommentFactory(
|
||||||
|
thread__document=document, user=random.choice([user, None])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert comment.get_abilities(user) == {
|
||||||
|
"destroy": True,
|
||||||
|
"update": True,
|
||||||
|
"partial_update": True,
|
||||||
|
"reactions": True,
|
||||||
|
"retrieve": True,
|
||||||
|
}
|
||||||
@@ -123,7 +123,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed():
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_last_on_child(
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ def test_models_document_access_get_abilities_for_owner_of_owner():
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ def test_models_document_access_get_abilities_for_owner_of_administrator():
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ def test_models_document_access_get_abilities_for_owner_of_editor():
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ def test_models_document_access_get_abilities_for_owner_of_reader():
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator", "owner"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator", "owner"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ def test_models_document_access_get_abilities_for_administrator_of_administrator
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ def test_models_document_access_get_abilities_for_administrator_of_editor():
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ def test_models_document_access_get_abilities_for_administrator_of_reader():
|
|||||||
"retrieve": True,
|
"retrieve": True,
|
||||||
"update": True,
|
"update": True,
|
||||||
"partial_update": True,
|
"partial_update": True,
|
||||||
"set_role_to": ["reader", "editor", "administrator"],
|
"set_role_to": ["reader", "commenter", "editor", "administrator"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -134,10 +134,13 @@ def test_models_documents_soft_delete(depth):
|
|||||||
[
|
[
|
||||||
(True, "restricted", "reader"),
|
(True, "restricted", "reader"),
|
||||||
(True, "restricted", "editor"),
|
(True, "restricted", "editor"),
|
||||||
|
(True, "restricted", "commenter"),
|
||||||
(False, "restricted", "reader"),
|
(False, "restricted", "reader"),
|
||||||
(False, "restricted", "editor"),
|
(False, "restricted", "editor"),
|
||||||
|
(False, "restricted", "commenter"),
|
||||||
(False, "authenticated", "reader"),
|
(False, "authenticated", "reader"),
|
||||||
(False, "authenticated", "editor"),
|
(False, "authenticated", "editor"),
|
||||||
|
(False, "authenticated", "commenter"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_models_documents_get_abilities_forbidden(
|
def test_models_documents_get_abilities_forbidden(
|
||||||
@@ -165,6 +168,7 @@ def test_models_documents_get_abilities_forbidden(
|
|||||||
"destroy": False,
|
"destroy": False,
|
||||||
"duplicate": False,
|
"duplicate": False,
|
||||||
"favorite": False,
|
"favorite": False,
|
||||||
|
"comment": False,
|
||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"mask": False,
|
"mask": False,
|
||||||
"media_auth": False,
|
"media_auth": False,
|
||||||
@@ -172,8 +176,8 @@ def test_models_documents_get_abilities_forbidden(
|
|||||||
"move": False,
|
"move": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"partial_update": False,
|
"partial_update": False,
|
||||||
@@ -223,6 +227,7 @@ def test_models_documents_get_abilities_reader(
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": False,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -232,8 +237,78 @@ def test_models_documents_get_abilities_reader(
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
|
"restricted": None,
|
||||||
|
},
|
||||||
|
"mask": is_authenticated,
|
||||||
|
"media_auth": True,
|
||||||
|
"media_check": True,
|
||||||
|
"move": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"restore": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"tree": True,
|
||||||
|
"update": False,
|
||||||
|
"versions_destroy": False,
|
||||||
|
"versions_list": False,
|
||||||
|
"versions_retrieve": False,
|
||||||
|
}
|
||||||
|
nb_queries = 1 if is_authenticated else 0
|
||||||
|
with django_assert_num_queries(nb_queries):
|
||||||
|
assert document.get_abilities(user) == expected_abilities
|
||||||
|
|
||||||
|
document.soft_delete()
|
||||||
|
document.refresh_from_db()
|
||||||
|
assert all(
|
||||||
|
value is False
|
||||||
|
for key, value in document.get_abilities(user).items()
|
||||||
|
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"is_authenticated,reach",
|
||||||
|
[
|
||||||
|
(True, "public"),
|
||||||
|
(False, "public"),
|
||||||
|
(True, "authenticated"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_models_documents_get_abilities_commenter(
|
||||||
|
is_authenticated, reach, django_assert_num_queries
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Check abilities returned for a document giving commenter role to link holders
|
||||||
|
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||||
|
"""
|
||||||
|
document = factories.DocumentFactory(link_reach=reach, link_role="commenter")
|
||||||
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
|
expected_abilities = {
|
||||||
|
"accesses_manage": False,
|
||||||
|
"accesses_view": False,
|
||||||
|
"ai_transform": False,
|
||||||
|
"ai_translate": False,
|
||||||
|
"attachment_upload": False,
|
||||||
|
"can_edit": False,
|
||||||
|
"children_create": False,
|
||||||
|
"children_list": True,
|
||||||
|
"collaboration_auth": True,
|
||||||
|
"comment": True,
|
||||||
|
"content": True,
|
||||||
|
"descendants": True,
|
||||||
|
"cors_proxy": True,
|
||||||
|
"destroy": False,
|
||||||
|
"duplicate": is_authenticated,
|
||||||
|
"favorite": is_authenticated,
|
||||||
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
|
"link_select_options": {
|
||||||
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": is_authenticated,
|
"mask": is_authenticated,
|
||||||
@@ -289,6 +364,7 @@ def test_models_documents_get_abilities_editor(
|
|||||||
"children_create": is_authenticated,
|
"children_create": is_authenticated,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": True,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -298,8 +374,8 @@ def test_models_documents_get_abilities_editor(
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": is_authenticated,
|
"mask": is_authenticated,
|
||||||
@@ -344,6 +420,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
|||||||
"children_create": True,
|
"children_create": True,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": True,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -353,8 +430,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
|||||||
"invite_owner": True,
|
"invite_owner": True,
|
||||||
"link_configuration": True,
|
"link_configuration": True,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": True,
|
"mask": True,
|
||||||
@@ -375,8 +452,43 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
|||||||
|
|
||||||
document.soft_delete()
|
document.soft_delete()
|
||||||
document.refresh_from_db()
|
document.refresh_from_db()
|
||||||
expected_abilities["move"] = False
|
assert document.get_abilities(user) == {
|
||||||
assert document.get_abilities(user) == expected_abilities
|
"accesses_manage": False,
|
||||||
|
"accesses_view": False,
|
||||||
|
"ai_transform": False,
|
||||||
|
"ai_translate": False,
|
||||||
|
"attachment_upload": False,
|
||||||
|
"can_edit": False,
|
||||||
|
"children_create": False,
|
||||||
|
"children_list": False,
|
||||||
|
"collaboration_auth": False,
|
||||||
|
"comment": False,
|
||||||
|
"descendants": False,
|
||||||
|
"cors_proxy": False,
|
||||||
|
"content": False,
|
||||||
|
"destroy": False,
|
||||||
|
"duplicate": False,
|
||||||
|
"favorite": False,
|
||||||
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
|
"link_select_options": {
|
||||||
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
|
"public": ["reader", "commenter", "editor"],
|
||||||
|
"restricted": None,
|
||||||
|
},
|
||||||
|
"mask": False,
|
||||||
|
"media_auth": False,
|
||||||
|
"media_check": False,
|
||||||
|
"move": False,
|
||||||
|
"partial_update": False,
|
||||||
|
"restore": True,
|
||||||
|
"retrieve": True,
|
||||||
|
"tree": True,
|
||||||
|
"update": False,
|
||||||
|
"versions_destroy": False,
|
||||||
|
"versions_list": False,
|
||||||
|
"versions_retrieve": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@@ -396,6 +508,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
|||||||
"children_create": True,
|
"children_create": True,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": True,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -405,8 +518,8 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": True,
|
"link_configuration": True,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": True,
|
"mask": True,
|
||||||
@@ -451,6 +564,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
|||||||
"children_create": True,
|
"children_create": True,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": True,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -460,8 +574,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": True,
|
"mask": True,
|
||||||
@@ -513,6 +627,8 @@ def test_models_documents_get_abilities_reader_user(
|
|||||||
"children_create": access_from_link,
|
"children_create": access_from_link,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": document.link_reach != "restricted"
|
||||||
|
and document.link_role in ["commenter", "editor"],
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -522,8 +638,73 @@ def test_models_documents_get_abilities_reader_user(
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
|
"restricted": None,
|
||||||
|
},
|
||||||
|
"mask": True,
|
||||||
|
"media_auth": True,
|
||||||
|
"media_check": True,
|
||||||
|
"move": False,
|
||||||
|
"partial_update": access_from_link,
|
||||||
|
"restore": False,
|
||||||
|
"retrieve": True,
|
||||||
|
"tree": True,
|
||||||
|
"update": access_from_link,
|
||||||
|
"versions_destroy": False,
|
||||||
|
"versions_list": True,
|
||||||
|
"versions_retrieve": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
with override_settings(AI_ALLOW_REACH_FROM=ai_access_setting):
|
||||||
|
with django_assert_num_queries(1):
|
||||||
|
assert document.get_abilities(user) == expected_abilities
|
||||||
|
|
||||||
|
document.soft_delete()
|
||||||
|
document.refresh_from_db()
|
||||||
|
assert all(
|
||||||
|
value is False
|
||||||
|
for key, value in document.get_abilities(user).items()
|
||||||
|
if key not in ["link_select_options", "ancestors_links_definition"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("ai_access_setting", ["public", "authenticated", "restricted"])
|
||||||
|
def test_models_documents_get_abilities_commenter_user(
|
||||||
|
ai_access_setting, django_assert_num_queries
|
||||||
|
):
|
||||||
|
"""Check abilities returned for the commenter of a document."""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
document = factories.DocumentFactory(users=[(user, "commenter")])
|
||||||
|
|
||||||
|
access_from_link = (
|
||||||
|
document.link_reach != "restricted" and document.link_role == "editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_abilities = {
|
||||||
|
"accesses_manage": False,
|
||||||
|
"accesses_view": True,
|
||||||
|
# If you get your editor rights from the link role and not your access role
|
||||||
|
# You should not access AI if it's restricted to users with specific access
|
||||||
|
"ai_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,
|
||||||
|
"comment": True,
|
||||||
|
"content": True,
|
||||||
|
"descendants": True,
|
||||||
|
"cors_proxy": True,
|
||||||
|
"destroy": False,
|
||||||
|
"duplicate": True,
|
||||||
|
"favorite": True,
|
||||||
|
"invite_owner": False,
|
||||||
|
"link_configuration": False,
|
||||||
|
"link_select_options": {
|
||||||
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": True,
|
"mask": True,
|
||||||
@@ -573,6 +754,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": True,
|
"children_list": True,
|
||||||
"collaboration_auth": True,
|
"collaboration_auth": True,
|
||||||
|
"comment": False,
|
||||||
"descendants": True,
|
"descendants": True,
|
||||||
"cors_proxy": True,
|
"cors_proxy": True,
|
||||||
"content": True,
|
"content": True,
|
||||||
@@ -582,8 +764,8 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
|||||||
"invite_owner": False,
|
"invite_owner": False,
|
||||||
"link_configuration": False,
|
"link_configuration": False,
|
||||||
"link_select_options": {
|
"link_select_options": {
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
"mask": True,
|
"mask": True,
|
||||||
@@ -1286,7 +1468,14 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
|||||||
"public",
|
"public",
|
||||||
"reader",
|
"reader",
|
||||||
{
|
{
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"public",
|
||||||
|
"commenter",
|
||||||
|
{
|
||||||
|
"public": ["commenter", "editor"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
("public", "editor", {"public": ["editor"]}),
|
("public", "editor", {"public": ["editor"]}),
|
||||||
@@ -1294,8 +1483,16 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
|||||||
"authenticated",
|
"authenticated",
|
||||||
"reader",
|
"reader",
|
||||||
{
|
{
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"authenticated",
|
||||||
|
"commenter",
|
||||||
|
{
|
||||||
|
"authenticated": ["commenter", "editor"],
|
||||||
|
"public": ["commenter", "editor"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -1308,8 +1505,17 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
|||||||
"reader",
|
"reader",
|
||||||
{
|
{
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"restricted",
|
||||||
|
"commenter",
|
||||||
|
{
|
||||||
|
"restricted": None,
|
||||||
|
"authenticated": ["commenter", "editor"],
|
||||||
|
"public": ["commenter", "editor"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -1326,15 +1532,15 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
|||||||
"public",
|
"public",
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
"reader",
|
"reader",
|
||||||
{
|
{
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1342,8 +1548,8 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries):
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"public": ["reader", "editor"],
|
"public": ["reader", "commenter", "editor"],
|
||||||
"authenticated": ["reader", "editor"],
|
"authenticated": ["reader", "commenter", "editor"],
|
||||||
"restricted": None,
|
"restricted": None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -26,20 +26,22 @@ document_related_router.register(
|
|||||||
viewsets.InvitationViewset,
|
viewsets.InvitationViewset,
|
||||||
basename="invitations",
|
basename="invitations",
|
||||||
)
|
)
|
||||||
|
document_related_router.register(
|
||||||
|
"threads",
|
||||||
|
viewsets.ThreadViewSet,
|
||||||
|
basename="threads",
|
||||||
|
)
|
||||||
document_related_router.register(
|
document_related_router.register(
|
||||||
"ask-for-access",
|
"ask-for-access",
|
||||||
viewsets.DocumentAskForAccessViewSet,
|
viewsets.DocumentAskForAccessViewSet,
|
||||||
basename="ask_for_access",
|
basename="ask_for_access",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
thread_related_router = DefaultRouter()
|
||||||
# - Routes nested under a template
|
thread_related_router.register(
|
||||||
template_related_router = DefaultRouter()
|
"comments",
|
||||||
template_related_router.register(
|
viewsets.CommentViewSet,
|
||||||
"accesses",
|
basename="comments",
|
||||||
viewsets.TemplateAccessViewSet,
|
|
||||||
basename="template_accesses",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -55,8 +57,8 @@ urlpatterns = [
|
|||||||
include(document_related_router.urls),
|
include(document_related_router.urls),
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^templates/(?P<resource_id>[0-9a-z-]*)/",
|
r"^documents/(?P<resource_id>[0-9a-z-]*)/threads/(?P<thread_id>[0-9a-z-]*)/",
|
||||||
include(template_related_router.urls),
|
include(thread_related_router.urls),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ class Base(Configuration):
|
|||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value(
|
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue(
|
||||||
60 * 60 * 24,
|
60 * 60 * 24,
|
||||||
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
|
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Breton\n"
|
"Language-Team: Breton\n"
|
||||||
"Language: br_FR\n"
|
"Language: br_FR\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Titouroù personel"
|
msgstr "Titouroù personel"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Aotreoù"
|
msgstr "Aotreoù"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Deiziadoù a-bouez"
|
msgstr "Deiziadoù a-bouez"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Gwezennadur"
|
msgstr "Gwezennadur"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr "Kuzhet"
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Sinedoù"
|
msgstr "Sinedoù"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
|
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
|
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr "Ar vaezienn-mañ a zo rekis."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Korf"
|
msgstr "Korf"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Doare korf"
|
msgstr "Doare korf"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Stumm"
|
msgstr "Stumm"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "eilenn {title}"
|
msgstr "eilenn {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "anv klok"
|
msgstr "anv klok"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "anv berr"
|
msgstr "anv berr"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "postel identelezh"
|
msgstr "postel identelezh"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "postel ar merour"
|
msgstr "postel ar merour"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "yezh"
|
msgstr "yezh"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
|
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
|
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "trevnad"
|
msgstr "trevnad"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
|
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "statud ar skipailh"
|
msgstr "statud ar skipailh"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
|
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "oberiant"
|
msgstr "oberiant"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
|
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "implijer"
|
msgstr "implijer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "implijerien"
|
msgstr "implijerien"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titl"
|
msgstr "titl"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "bomm"
|
msgstr "bomm"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Restr"
|
msgstr "Restr"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Restroù"
|
msgstr "Restroù"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Restr hep titl"
|
msgstr "Restr hep titl"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
|
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
|
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Roud liamm ar restr/an implijer"
|
msgstr "Roud liamm ar restr/an implijer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Roudoù liamm ar restr/an implijer"
|
msgstr "Roudoù liamm ar restr/an implijer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
|
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Restr muiañ-karet"
|
msgstr "Restr muiañ-karet"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Restroù muiañ-karet"
|
msgstr "Restroù muiañ-karet"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
|
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Liamm restr/implijer"
|
msgstr "Liamm restr/implijer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Liammoù restr/implijer"
|
msgstr "Liammoù restr/implijer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
|
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr "Goulenn tizhout ar restr"
|
msgstr "Goulenn tizhout ar restr"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr "Goulennoù tizhout ar restr"
|
msgstr "Goulennoù tizhout ar restr"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
|
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
|
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
|
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
|
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "deskrivadur"
|
msgstr "deskrivadur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "kod"
|
msgstr "kod"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "publik"
|
msgstr "publik"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
|
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Patrom"
|
msgstr "Patrom"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Patromoù"
|
msgstr "Patromoù"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Liamm patrom/implijer"
|
msgstr "Liamm patrom/implijer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Liammoù patrom/implijer"
|
msgstr "Liammoù patrom/implijer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "An implijer-mañ a zo dija er patrom-mañ."
|
msgstr "An implijer-mañ a zo dija er patrom-mañ."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
|
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "postel"
|
msgstr "postel"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Pedadenn d'ur restr"
|
msgstr "Pedadenn d'ur restr"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Pedadennoù d'ur restr"
|
msgstr "Pedadennoù d'ur restr"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
|
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo ar postel"
|
msgstr "Logo ar postel"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Digeriñ"
|
msgstr "Digeriñ"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
|
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: German\n"
|
"Language-Team: German\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Persönliche Daten"
|
msgstr "Persönliche Daten"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Berechtigungen"
|
msgstr "Berechtigungen"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Wichtige Daten"
|
msgstr "Wichtige Daten"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Baumstruktur"
|
msgstr "Baumstruktur"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Favorit"
|
msgstr "Favorit"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Inhalt"
|
msgstr "Inhalt"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Typ"
|
msgstr "Typ"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Format"
|
msgstr "Format"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "Kopie von {title}"
|
msgstr "Kopie von {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "Kurzbezeichnung"
|
msgstr "Kurzbezeichnung"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "Identitäts-E-Mail-Adresse"
|
msgstr "Identitäts-E-Mail-Adresse"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "Admin E-Mail-Adresse"
|
msgstr "Admin E-Mail-Adresse"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "Sprache"
|
msgstr "Sprache"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
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."
|
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
|
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "Gerät"
|
msgstr "Gerät"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
|
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "Status des Teammitgliedes"
|
msgstr "Status des Teammitgliedes"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
|
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "aktiviert"
|
msgstr "aktiviert"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
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."
|
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "Benutzer"
|
msgstr "Benutzer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "Benutzer"
|
msgstr "Benutzer"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "Titel"
|
msgstr "Titel"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "Auszug"
|
msgstr "Auszug"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Dokument"
|
msgstr "Dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Dokumente"
|
msgstr "Dokumente"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Unbenanntes Dokument"
|
msgstr "Unbenanntes Dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
|
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
|
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
|
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Dokumentenfavorit"
|
msgstr "Dokumentenfavorit"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Dokumentfavoriten"
|
msgstr "Dokumentfavoriten"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
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."
|
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Dokument/Benutzerbeziehung"
|
msgstr "Dokument/Benutzerbeziehung"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Dokument/Benutzerbeziehungen"
|
msgstr "Dokument/Benutzerbeziehungen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
|
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "Beschreibung"
|
msgstr "Beschreibung"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "Code"
|
msgstr "Code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "CSS"
|
msgstr "CSS"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "öffentlich"
|
msgstr "öffentlich"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Vorlage"
|
msgstr "Vorlage"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Vorlagen"
|
msgstr "Vorlagen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Vorlage/Benutzer-Beziehung"
|
msgstr "Vorlage/Benutzer-Beziehung"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Vorlage/Benutzerbeziehungen"
|
msgstr "Vorlage/Benutzerbeziehungen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Dieses Team ist bereits in diesem Template."
|
msgstr "Dieses Team ist bereits in diesem Template."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "E-Mail-Adresse"
|
msgstr "E-Mail-Adresse"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Einladung zum Dokument"
|
msgstr "Einladung zum Dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Dokumenteinladungen"
|
msgstr "Dokumenteinladungen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo-E-Mail"
|
msgstr "Logo-E-Mail"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Öffnen"
|
msgstr "Öffnen"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
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. "
|
msgstr " Docs, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
"Language: en_US\n"
|
"Language: en_US\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Spanish\n"
|
"Language-Team: Spanish\n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Información Personal"
|
msgstr "Información Personal"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Permisos"
|
msgstr "Permisos"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Fechas importantes"
|
msgstr "Fechas importantes"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Estructura en árbol"
|
msgstr "Estructura en árbol"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Favorito"
|
msgstr "Favorito"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "¡Un nuevo documento se ha creado por ti!"
|
msgstr "¡Un nuevo documento se ha creado por ti!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
|
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Cuerpo"
|
msgstr "Cuerpo"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Tipo de Cuerpo"
|
msgstr "Tipo de Cuerpo"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formato"
|
msgstr "Formato"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "copia de {title}"
|
msgstr "copia de {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "nombre completo"
|
msgstr "nombre completo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "nombre abreviado"
|
msgstr "nombre abreviado"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "correo electrónico de identidad"
|
msgstr "correo electrónico de identidad"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "correo electrónico del administrador"
|
msgstr "correo electrónico del administrador"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "idioma"
|
msgstr "idioma"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "El idioma en el que el usuario desea ver la interfaz."
|
msgstr "El idioma en el que el usuario desea ver la interfaz."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
|
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "dispositivo"
|
msgstr "dispositivo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Si el usuario es un dispositivo o un usuario real."
|
msgstr "Si el usuario es un dispositivo o un usuario real."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "rol en el equipo"
|
msgstr "rol en el equipo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
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."
|
msgstr "Si el usuario puede iniciar sesión en esta página web de administración."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "activo"
|
msgstr "activo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
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."
|
msgstr "Si este usuario debe ser considerado como activo. Deseleccionar en lugar de eliminar cuentas."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "usuario"
|
msgstr "usuario"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "usuarios"
|
msgstr "usuarios"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "título"
|
msgstr "título"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "resumen"
|
msgstr "resumen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Documento"
|
msgstr "Documento"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Documentos"
|
msgstr "Documentos"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Documento sin título"
|
msgstr "Documento sin título"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "¡{name} ha compartido un documento contigo!"
|
msgstr "¡{name} ha compartido un documento contigo!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
|
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Traza del enlace de documento/usuario"
|
msgstr "Traza del enlace de documento/usuario"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Trazas del enlace de documento/usuario"
|
msgstr "Trazas del enlace de documento/usuario"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Ya existe una traza de enlace para este documento/usuario."
|
msgstr "Ya existe una traza de enlace para este documento/usuario."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Documento favorito"
|
msgstr "Documento favorito"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Documentos favoritos"
|
msgstr "Documentos favoritos"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
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."
|
msgstr "Este documento ya ha sido marcado como favorito por el usuario."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Relación documento/usuario"
|
msgstr "Relación documento/usuario"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Relaciones documento/usuario"
|
msgstr "Relaciones documento/usuario"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "Este usuario ya forma parte del documento."
|
msgstr "Este usuario ya forma parte del documento."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Este equipo ya forma parte del documento."
|
msgstr "Este equipo ya forma parte del documento."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Debe establecerse un usuario o un equipo, no ambos."
|
msgstr "Debe establecerse un usuario o un equipo, no ambos."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr "Solicitud de acceso"
|
msgstr "Solicitud de acceso"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr "Solicitud de accesos"
|
msgstr "Solicitud de accesos"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr "Este usuario ya ha solicitado acceso a este documento."
|
msgstr "Este usuario ya ha solicitado acceso a este documento."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr "¡{name} desea acceder a un documento!"
|
msgstr "¡{name} desea acceder a un documento!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr "{name} desea acceso al siguiente documento:"
|
msgstr "{name} desea acceso al siguiente documento:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr "{name} está pidiendo acceso al documento: {title}"
|
msgstr "{name} está pidiendo acceso al documento: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "descripción"
|
msgstr "descripción"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "código"
|
msgstr "código"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "público"
|
msgstr "público"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
|
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Plantilla"
|
msgstr "Plantilla"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Plantillas"
|
msgstr "Plantillas"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Relación plantilla/usuario"
|
msgstr "Relación plantilla/usuario"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Relaciones plantilla/usuario"
|
msgstr "Relaciones plantilla/usuario"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "Este usuario ya forma parte de la plantilla."
|
msgstr "Este usuario ya forma parte de la plantilla."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Este equipo ya se encuentra en esta plantilla."
|
msgstr "Este equipo ya se encuentra en esta plantilla."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "dirección de correo electrónico"
|
msgstr "dirección de correo electrónico"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Invitación al documento"
|
msgstr "Invitación al documento"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Invitaciones a documentos"
|
msgstr "Invitaciones a documentos"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Este correo electrónico está asociado a un usuario registrado."
|
msgstr "Este correo electrónico está asociado a un usuario registrado."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo de correo electrónico"
|
msgstr "Logo de correo electrónico"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Abrir"
|
msgstr "Abrir"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
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."
|
msgstr "Docs, su nueva herramienta esencial para organizar, compartir y colaborar en sus documentos como equipo."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: French\n"
|
"Language-Team: French\n"
|
||||||
"Language: fr_FR\n"
|
"Language: fr_FR\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Infos Personnelles"
|
msgstr "Infos Personnelles"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Permissions"
|
msgstr "Permissions"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Dates importantes"
|
msgstr "Dates importantes"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Arborescence"
|
msgstr "Arborescence"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr "Masqué"
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Favoris"
|
msgstr "Favoris"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Un nouveau document a été créé pour vous !"
|
msgstr "Un nouveau document a été créé pour vous !"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
|
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr "Ce champ est obligatoire."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Corps"
|
msgstr "Corps"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Type de corps"
|
msgstr "Type de corps"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Format"
|
msgstr "Format"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "copie de {title}"
|
msgstr "copie de {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr "Obligatoire. 255 caractères ou moins. Caractères ASCII uniquement."
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "nom complet"
|
msgstr "nom complet"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "nom court"
|
msgstr "nom court"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "adresse e-mail d'identité"
|
msgstr "adresse e-mail d'identité"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "adresse e-mail de l'administrateur"
|
msgstr "adresse e-mail de l'administrateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "langue"
|
msgstr "langue"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
|
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "appareil"
|
msgstr "appareil"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "statut d'équipe"
|
msgstr "statut d'équipe"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "actif"
|
msgstr "actif"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
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."
|
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "utilisateur"
|
msgstr "utilisateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "utilisateurs"
|
msgstr "utilisateurs"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titre"
|
msgstr "titre"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "extrait"
|
msgstr "extrait"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Document"
|
msgstr "Document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Documents"
|
msgstr "Documents"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Document sans titre"
|
msgstr "Document sans titre"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} a partagé un document avec vous!"
|
msgstr "{name} a partagé un document avec vous!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
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 :"
|
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant :"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} a partagé un document avec vous : {title}"
|
msgstr "{name} a partagé un document avec vous : {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Trace du lien document/utilisateur"
|
msgstr "Trace du lien document/utilisateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Traces du lien document/utilisateur"
|
msgstr "Traces du lien document/utilisateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
|
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Document favori"
|
msgstr "Document favori"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Documents favoris"
|
msgstr "Documents favoris"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
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."
|
msgstr "Ce document est déjà un favori de cet utilisateur."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Relation document/utilisateur"
|
msgstr "Relation document/utilisateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Relations document/utilisateur"
|
msgstr "Relations document/utilisateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "Cet utilisateur est déjà dans ce document."
|
msgstr "Cet utilisateur est déjà dans ce document."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Cette équipe est déjà dans ce document."
|
msgstr "Cette équipe est déjà dans ce document."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
|
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr "Demande d'accès au document"
|
msgstr "Demande d'accès au document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr "Demande d'accès au document"
|
msgstr "Demande d'accès au document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
|
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr "{name} souhaiterait accéder au document suivant !"
|
msgstr "{name} souhaiterait accéder au document suivant !"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr "{name} souhaiterait accéder au document suivant :"
|
msgstr "{name} souhaiterait accéder au document suivant :"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr "{name} demande l'accès au document : {title}"
|
msgstr "{name} demande l'accès au document : {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "description"
|
msgstr "description"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "code"
|
msgstr "code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "CSS"
|
msgstr "CSS"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "public"
|
msgstr "public"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Si ce modèle est public, utilisable par n'importe qui."
|
msgstr "Si ce modèle est public, utilisable par n'importe qui."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Modèle"
|
msgstr "Modèle"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Modèles"
|
msgstr "Modèles"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Relation modèle/utilisateur"
|
msgstr "Relation modèle/utilisateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Relations modèle/utilisateur"
|
msgstr "Relations modèle/utilisateur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "Cet utilisateur est déjà dans ce modèle."
|
msgstr "Cet utilisateur est déjà dans ce modèle."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Cette équipe est déjà modèle."
|
msgstr "Cette équipe est déjà modèle."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "adresse e-mail"
|
msgstr "adresse e-mail"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Invitation à un document"
|
msgstr "Invitation à un document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Invitations à un document"
|
msgstr "Invitations à un document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
|
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo de l'e-mail"
|
msgstr "Logo de l'e-mail"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Ouvrir"
|
msgstr "Ouvrir"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
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. "
|
msgstr " Docs, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Italian\n"
|
"Language-Team: Italian\n"
|
||||||
"Language: it_IT\n"
|
"Language: it_IT\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Informazioni personali"
|
msgstr "Informazioni personali"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Permessi"
|
msgstr "Permessi"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Date importanti"
|
msgstr "Date importanti"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Struttura ad albero"
|
msgstr "Struttura ad albero"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Preferiti"
|
msgstr "Preferiti"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Un nuovo documento è stato creato a tuo nome!"
|
msgstr "Un nuovo documento è stato creato a tuo nome!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Sei ora proprietario di un nuovo documento:"
|
msgstr "Sei ora proprietario di un nuovo documento:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Corpo"
|
msgstr "Corpo"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formato"
|
msgstr "Formato"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "copia di {title}"
|
msgstr "copia di {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "nome completo"
|
msgstr "nome completo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "nome"
|
msgstr "nome"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "indirizzo email di identità"
|
msgstr "indirizzo email di identità"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "Indirizzo email dell'amministratore"
|
msgstr "Indirizzo email dell'amministratore"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "lingua"
|
msgstr "lingua"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
|
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
|
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "dispositivo"
|
msgstr "dispositivo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Se l'utente è un dispositivo o un utente reale."
|
msgstr "Se l'utente è un dispositivo o un utente reale."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "stato del personale"
|
msgstr "stato del personale"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Indica se l'utente può accedere a questo sito amministratore."
|
msgstr "Indica se l'utente può accedere a questo sito amministratore."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "attivo"
|
msgstr "attivo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
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."
|
msgstr "Indica se questo utente deve essere trattato come attivo. Deseleziona invece di eliminare gli account."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "utente"
|
msgstr "utente"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "utenti"
|
msgstr "utenti"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titolo"
|
msgstr "titolo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Documento"
|
msgstr "Documento"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Documenti"
|
msgstr "Documenti"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Documento senza titolo"
|
msgstr "Documento senza titolo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} ha condiviso un documento con te!"
|
msgstr "{name} ha condiviso un documento con te!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
|
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} ha condiviso un documento con te: {title}"
|
msgstr "{name} ha condiviso un documento con te: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Documento preferito"
|
msgstr "Documento preferito"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Documenti preferiti"
|
msgstr "Documenti preferiti"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "Questo utente è già presente in questo documento."
|
msgstr "Questo utente è già presente in questo documento."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Questo team è già presente in questo documento."
|
msgstr "Questo team è già presente in questo documento."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "descrizione"
|
msgstr "descrizione"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "code"
|
msgstr "code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "pubblico"
|
msgstr "pubblico"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Indica se questo modello è pubblico per chiunque."
|
msgstr "Indica se questo modello è pubblico per chiunque."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Modello"
|
msgstr "Modello"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Modelli"
|
msgstr "Modelli"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "Questo utente è già in questo modello."
|
msgstr "Questo utente è già in questo modello."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Questo team è già in questo modello."
|
msgstr "Questo team è già in questo modello."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "indirizzo e-mail"
|
msgstr "indirizzo e-mail"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Invito al documento"
|
msgstr "Invito al documento"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Inviti al documento"
|
msgstr "Inviti al documento"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Questa email è già associata a un utente registrato."
|
msgstr "Questa email è già associata a un utente registrato."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo e-mail"
|
msgstr "Logo e-mail"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Apri"
|
msgstr "Apri"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Dutch\n"
|
"Language-Team: Dutch\n"
|
||||||
"Language: nl_NL\n"
|
"Language: nl_NL\n"
|
||||||
@@ -17,22 +17,22 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Persoonlijke informatie"
|
msgstr "Persoonlijke informatie"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Toestemmingen"
|
msgstr "Machtigingen"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Belangrijke datums"
|
msgstr "Belangrijke data"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Document structuur"
|
msgstr "Boomstructuur"
|
||||||
|
|
||||||
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
|
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
|
||||||
msgid "Title"
|
msgid "Title"
|
||||||
@@ -40,37 +40,46 @@ msgstr "Titel"
|
|||||||
|
|
||||||
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
|
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
|
||||||
msgid "Creator is me"
|
msgid "Creator is me"
|
||||||
msgstr "Ik ben Eigenaar"
|
msgstr "Ik ben eigenaar"
|
||||||
|
|
||||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||||
msgid "Masked"
|
msgid "Masked"
|
||||||
msgstr ""
|
msgstr "Gemaskeerd"
|
||||||
|
|
||||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Favoriete"
|
msgstr "Favoriet"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Een nieuw document was gecreëerd voor u!"
|
msgstr "Een nieuw document is namens u gemaakt!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "U heeft eigenaarschap van een nieuw document:"
|
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr "Dit veld is verplicht."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Text"
|
msgstr "Text"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Text type"
|
msgstr "Text type"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formaat"
|
msgstr "Formaat"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "kopie van {title}"
|
msgstr "kopie van {title}"
|
||||||
@@ -83,11 +92,11 @@ msgstr "Lezer"
|
|||||||
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
|
||||||
#: core/choices.py:43
|
#: core/choices.py:43
|
||||||
msgid "Editor"
|
msgid "Editor"
|
||||||
msgstr "Bewerker"
|
msgstr "Redacteur"
|
||||||
|
|
||||||
#: build/lib/core/choices.py:44 core/choices.py:44
|
#: build/lib/core/choices.py:44 core/choices.py:44
|
||||||
msgid "Administrator"
|
msgid "Administrator"
|
||||||
msgstr "Administrator"
|
msgstr "Beheerder"
|
||||||
|
|
||||||
#: build/lib/core/choices.py:45 core/choices.py:45
|
#: build/lib/core/choices.py:45 core/choices.py:45
|
||||||
msgid "Owner"
|
msgid "Owner"
|
||||||
@@ -95,7 +104,7 @@ msgstr "Eigenaar"
|
|||||||
|
|
||||||
#: build/lib/core/choices.py:56 core/choices.py:56
|
#: build/lib/core/choices.py:56 core/choices.py:56
|
||||||
msgid "Restricted"
|
msgid "Restricted"
|
||||||
msgstr "Niet toegestaan"
|
msgstr "Beperkt"
|
||||||
|
|
||||||
#: build/lib/core/choices.py:60 core/choices.py:60
|
#: build/lib/core/choices.py:60 core/choices.py:60
|
||||||
msgid "Authenticated"
|
msgid "Authenticated"
|
||||||
@@ -139,11 +148,11 @@ msgstr "primaire sleutel voor dossier als UUID"
|
|||||||
|
|
||||||
#: build/lib/core/models.py:87 core/models.py:87
|
#: build/lib/core/models.py:87 core/models.py:87
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "gemaakt op"
|
msgstr "gecreëerd op"
|
||||||
|
|
||||||
#: build/lib/core/models.py:88 core/models.py:88
|
#: build/lib/core/models.py:88 core/models.py:88
|
||||||
msgid "date and time at which a record was created"
|
msgid "date and time at which a record was created"
|
||||||
msgstr "datum en tijd wanneer dossier was gecreëerd"
|
msgstr "datum en tijd waarop dossier is gecreeërd"
|
||||||
|
|
||||||
#: build/lib/core/models.py:93 core/models.py:93
|
#: build/lib/core/models.py:93 core/models.py:93
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
@@ -155,7 +164,7 @@ msgstr "datum en tijd waarop dossier laatst was gewijzigd"
|
|||||||
|
|
||||||
#: build/lib/core/models.py:130 core/models.py:130
|
#: build/lib/core/models.py:130 core/models.py:130
|
||||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
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."
|
msgstr "Wij konden geen gebruiker vinden met dit id, maar de email is al geassocieerd met een geregistreerde gebruiker."
|
||||||
|
|
||||||
#: build/lib/core/models.py:141 core/models.py:141
|
#: build/lib/core/models.py:141 core/models.py:141
|
||||||
msgid "sub"
|
msgid "sub"
|
||||||
@@ -163,253 +172,253 @@ msgstr "id"
|
|||||||
|
|
||||||
#: build/lib/core/models.py:142 core/models.py:142
|
#: build/lib/core/models.py:142 core/models.py:142
|
||||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||||
msgstr ""
|
msgstr "Vereist. 255 tekens of minder. Alleen ASCII tekens."
|
||||||
|
|
||||||
#: build/lib/core/models.py:150 core/models.py:150
|
#: build/lib/core/models.py:150 core/models.py:150
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "volledige naam"
|
msgstr "volledige naam"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "gebruikersnaam"
|
msgstr "gebruikersnaam"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "identiteit email adres"
|
msgstr "identiteit emailadres"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "admin email adres"
|
msgstr "admin emailadres"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "taal"
|
msgstr "taal"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "De taal waarin de gebruiker de interface wilt zien."
|
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "De tijdzone waarin de gebruiker de tijden wilt zien."
|
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "apparaat"
|
msgstr "apparaat"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "beheerder status"
|
msgstr "beheerder status"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Of de gebruiker kan inloggen in het admin gedeelte."
|
msgstr "Of de gebruiker kan inloggen in het beheer gedeelte."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "actief"
|
msgstr "actief"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
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."
|
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "gebruiker"
|
msgstr "gebruiker"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "gebruikers"
|
msgstr "gebruikers"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "titel"
|
msgstr "titel"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "uittreksel"
|
msgstr "uittreksel"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Document"
|
msgstr "Document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Documenten"
|
msgstr "Documenten"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Naamloos Document"
|
msgstr "Naamloos Document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} heeft een document met gedeeld!"
|
msgstr "{name} heeft een document met u gedeeld!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
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:"
|
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} heeft een document met u gedeeld: {title}"
|
msgstr "{name} heeft een document met u gedeeld: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Document/gebruiker url"
|
msgstr "Document/gebruiker link"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Document/gebruiker url"
|
msgstr "Document/gebruiker link"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Een url bestaat al voor dit document/deze gebruiker."
|
msgstr "Een link bestaat al voor dit document/deze gebruiker."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Document favoriet"
|
msgstr "Document favoriet"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Document favorieten"
|
msgstr "Document favorieten"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
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."
|
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Document/gebruiker relatie"
|
msgstr "Document/gebruiker relatie"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Document/gebruiker relaties"
|
msgstr "Document/gebruiker relaties"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "De gebruiker is al in dit document."
|
msgstr "De gebruiker bestaat al in dit document."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Het team is al in dit document."
|
msgstr "Dit team bestaat al in dit document."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
|
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr "Document verzoekt om toegang"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr "Document verzoekt om toegangen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr "{name} verzoekt toegang tot een document!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr "{name} verzoekt toegang tot het volgende document:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr "{name} verzoekt toegang tot het document: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "omschrijving"
|
msgstr "omschrijving"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "code"
|
msgstr "code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "publiek"
|
msgstr "publiek"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
|
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Template"
|
msgstr "Sjabloon"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Templates"
|
msgstr "Sjabloon"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Template/gebruiker relatie"
|
msgstr "Sjabloon/gebruiker relatie"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Template/gebruiker relaties"
|
msgstr "Sjabloon/gebruiker relaties"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "De gebruiker bestaat al in dit template."
|
msgstr "De gebruiker bestaat al in dit sjabloon."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Het team bestaat al in dit template."
|
msgstr "Het team bestaat al in dit sjabloon."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "email adres"
|
msgstr "e-mailadres"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Document uitnodiging"
|
msgstr "Document uitnodiging"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Document uitnodigingen"
|
msgstr "Document uitnodigingen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
|
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo email"
|
msgstr "Logo email"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Open"
|
msgstr "Open"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
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. "
|
msgstr " Docs, jouw nieuwe essentiële tool voor het organiseren, delen en collaboreren van documenten als team. "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Portuguese\n"
|
"Language-Team: Portuguese\n"
|
||||||
"Language: pt_PT\n"
|
"Language: pt_PT\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Informações Pessoais"
|
msgstr "Informações Pessoais"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Permissões"
|
msgstr "Permissões"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Datas importantes"
|
msgstr "Datas importantes"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Estrutura de árvore"
|
msgstr "Estrutura de árvore"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Favorito"
|
msgstr "Favorito"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Um novo documento foi criado em seu nome!"
|
msgstr "Um novo documento foi criado em seu nome!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "A propriedade de um novo documento foi concedida a você:"
|
msgstr "A propriedade de um novo documento foi concedida a você:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Corpo"
|
msgstr "Corpo"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Tipo de corpo"
|
msgstr "Tipo de corpo"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formato"
|
msgstr "Formato"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "cópia de {title}"
|
msgstr "cópia de {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Russian\n"
|
"Language-Team: Russian\n"
|
||||||
"Language: ru_RU\n"
|
"Language: ru_RU\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Личная информация"
|
msgstr "Личная информация"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Разрешения"
|
msgstr "Разрешения"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Важные даты"
|
msgstr "Важные даты"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Древовидная структура"
|
msgstr "Древовидная структура"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr "Скрытый"
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Избранное"
|
msgstr "Избранное"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Новый документ был создан от вашего имени!"
|
msgstr "Новый документ был создан от вашего имени!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Вы назначены владельцем для нового документа:"
|
msgstr "Вы назначены владельцем для нового документа:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr "Это поле обязательное."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Текст сообщения"
|
msgstr "Текст сообщения"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Тип сообщения"
|
msgstr "Тип сообщения"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Формат"
|
msgstr "Формат"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "копия {title}"
|
msgstr "копия {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr "Обязательно. 255 символов или меньше. Тол
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "полное имя"
|
msgstr "полное имя"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "короткое имя"
|
msgstr "короткое имя"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "личный адрес электронной почты"
|
msgstr "личный адрес электронной почты"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "e-mail администратора"
|
msgstr "e-mail администратора"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "язык"
|
msgstr "язык"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
|
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
|
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "устройство"
|
msgstr "устройство"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Пользователь является устройством или человеком."
|
msgstr "Пользователь является устройством или человеком."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "статус сотрудника"
|
msgstr "статус сотрудника"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Может ли пользователь войти на этот административный сайт."
|
msgstr "Может ли пользователь войти на этот административный сайт."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "активный"
|
msgstr "активный"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
|
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "пользователь"
|
msgstr "пользователь"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "пользователи"
|
msgstr "пользователи"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "заголовок"
|
msgstr "заголовок"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "отрывок"
|
msgstr "отрывок"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Документ"
|
msgstr "Документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Документы"
|
msgstr "Документы"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Безымянный документ"
|
msgstr "Безымянный документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} делится с вами документом!"
|
msgstr "{name} делится с вами документом!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} делится с вами документом: {title}"
|
msgstr "{name} делится с вами документом: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Трассировка связи документ/пользователь"
|
msgstr "Трассировка связи документ/пользователь"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Трассировка связей документ/пользователь"
|
msgstr "Трассировка связей документ/пользователь"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
|
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Избранный документ"
|
msgstr "Избранный документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Избранные документы"
|
msgstr "Избранные документы"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr "Этот документ уже помечен как избранный для этого пользователя."
|
msgstr "Этот документ уже помечен как избранный для этого пользователя."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Отношение документ/пользователь"
|
msgstr "Отношение документ/пользователь"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Отношения документ/пользователь"
|
msgstr "Отношения документ/пользователь"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Эта команда уже имеет доступ к этому документу."
|
msgstr "Эта команда уже имеет доступ к этому документу."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
|
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr "Документ запрашивает доступ"
|
msgstr "Документ запрашивает доступ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr "Документ запрашивает доступы"
|
msgstr "Документ запрашивает доступы"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr "Этот пользователь уже запросил доступ к этому документу."
|
msgstr "Этот пользователь уже запросил доступ к этому документу."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr "{name} хочет получить доступ к документу!"
|
msgstr "{name} хочет получить доступ к документу!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr "{name} хочет получить доступ к следующему документу:"
|
msgstr "{name} хочет получить доступ к следующему документу:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr "{name} запрашивает доступ к документу: {title}"
|
msgstr "{name} запрашивает доступ к документу: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "описание"
|
msgstr "описание"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "код"
|
msgstr "код"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "доступно всем"
|
msgstr "доступно всем"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Этот шаблон доступен всем пользователям."
|
msgstr "Этот шаблон доступен всем пользователям."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Шаблон"
|
msgstr "Шаблон"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Шаблоны"
|
msgstr "Шаблоны"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Отношение шаблон/пользователь"
|
msgstr "Отношение шаблон/пользователь"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Отношения шаблон/пользователь"
|
msgstr "Отношения шаблон/пользователь"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "Этот пользователь уже указан в этом шаблоне."
|
msgstr "Этот пользователь уже указан в этом шаблоне."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Эта команда уже указана в этом шаблоне."
|
msgstr "Эта команда уже указана в этом шаблоне."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "адрес электронной почты"
|
msgstr "адрес электронной почты"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Приглашение для документа"
|
msgstr "Приглашение для документа"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Приглашения для документов"
|
msgstr "Приглашения для документов"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
|
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Логотип email"
|
msgstr "Логотип email"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Открыть"
|
msgstr "Открыть"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr " Docs, ваш новый инструмент для организации и совместного использования документов в вашей команде. "
|
msgstr " Docs, ваш новый инструмент для организации и совместного использования документов в вашей команде. "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Slovenian\n"
|
"Language-Team: Slovenian\n"
|
||||||
"Language: sl_SI\n"
|
"Language: sl_SI\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Osebni podatki"
|
msgstr "Osebni podatki"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Dovoljenja"
|
msgstr "Dovoljenja"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Pomembni datumi"
|
msgstr "Pomembni datumi"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Drevesna struktura"
|
msgstr "Drevesna struktura"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Priljubljena"
|
msgstr "Priljubljena"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
|
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
|
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Telo"
|
msgstr "Telo"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Vrsta telesa"
|
msgstr "Vrsta telesa"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Oblika"
|
msgstr "Oblika"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "polno ime"
|
msgstr "polno ime"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "kratko ime"
|
msgstr "kratko ime"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "elektronski naslov identitete"
|
msgstr "elektronski naslov identitete"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "elektronski naslov skrbnika"
|
msgstr "elektronski naslov skrbnika"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "jezik"
|
msgstr "jezik"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
|
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
|
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "naprava"
|
msgstr "naprava"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
|
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "kadrovski status"
|
msgstr "kadrovski status"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
|
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "aktivni"
|
msgstr "aktivni"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
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."
|
msgstr "Ali je treba tega uporabnika obravnavati kot aktivnega. Namesto brisanja računov počistite to izbiro."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "uporabnik"
|
msgstr "uporabnik"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "uporabniki"
|
msgstr "uporabniki"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "naslov"
|
msgstr "naslov"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "odlomek"
|
msgstr "odlomek"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Dokument"
|
msgstr "Dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Dokumenti"
|
msgstr "Dokumenti"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Dokument brez naslova"
|
msgstr "Dokument brez naslova"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} je delil dokument z vami!"
|
msgstr "{name} je delil dokument z vami!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
|
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} je delil dokument z vami: {title}"
|
msgstr "{name} je delil dokument z vami: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Dokument/sled povezave uporabnika"
|
msgstr "Dokument/sled povezave uporabnika"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Sledi povezav dokumenta/uporabnika"
|
msgstr "Sledi povezav dokumenta/uporabnika"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Priljubljeni dokument"
|
msgstr "Priljubljeni dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Priljubljeni dokumenti"
|
msgstr "Priljubljeni dokumenti"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
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."
|
msgstr "Ta dokument je že ciljno usmerjen s priljubljenim primerkom relacije za istega uporabnika."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Odnos dokument/uporabnik"
|
msgstr "Odnos dokument/uporabnik"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Odnosi dokument/uporabnik"
|
msgstr "Odnosi dokument/uporabnik"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "Ta uporabnik je že v tem dokumentu."
|
msgstr "Ta uporabnik je že v tem dokumentu."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Ta ekipa je že v tem dokumentu."
|
msgstr "Ta ekipa je že v tem dokumentu."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
|
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "opis"
|
msgstr "opis"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "koda"
|
msgstr "koda"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "javno"
|
msgstr "javno"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Ali je ta predloga javna za uporabo."
|
msgstr "Ali je ta predloga javna za uporabo."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Predloga"
|
msgstr "Predloga"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Predloge"
|
msgstr "Predloge"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Odnos predloga/uporabnik"
|
msgstr "Odnos predloga/uporabnik"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Odnosi med predlogo in uporabnikom"
|
msgstr "Odnosi med predlogo in uporabnikom"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "Ta uporabnik je že v tej predlogi."
|
msgstr "Ta uporabnik je že v tej predlogi."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Ta ekipa je že v tej predlogi."
|
msgstr "Ta ekipa je že v tej predlogi."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "elektronski naslov"
|
msgstr "elektronski naslov"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Vabilo na dokument"
|
msgstr "Vabilo na dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Vabila na dokument"
|
msgstr "Vabila na dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
|
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "E-pošta z logotipom"
|
msgstr "E-pošta z logotipom"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Odpri"
|
msgstr "Odpri"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
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. "
|
msgstr " Dokumenti, vaše novo bistveno orodje za organiziranje, skupno rabo in skupinsko sodelovanje pri dokumentih. "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Swedish\n"
|
"Language-Team: Swedish\n"
|
||||||
"Language: sv_SE\n"
|
"Language: sv_SE\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Personuppgifter"
|
msgstr "Personuppgifter"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Behörigheter"
|
msgstr "Behörigheter"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Viktiga datum"
|
msgstr "Viktiga datum"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Favoriter"
|
msgstr "Favoriter"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Ett nytt dokument skapades åt dig!"
|
msgstr "Ett nytt dokument skapades åt dig!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
|
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Format"
|
msgstr "Format"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "aktiv"
|
msgstr "aktiv"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "e-postadress"
|
msgstr "e-postadress"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Bjud in dokument"
|
msgstr "Bjud in dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Inbjudningar dokument"
|
msgstr "Inbjudningar dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
|
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logotyp e-post"
|
msgstr "Logotyp e-post"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Öppna"
|
msgstr "Öppna"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Turkish\n"
|
"Language-Team: Turkish\n"
|
||||||
"Language: tr_TR\n"
|
"Language: tr_TR\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr ""
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -169,247 +178,247 @@ msgstr ""
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Ukrainian\n"
|
"Language-Team: Ukrainian\n"
|
||||||
"Language: uk_UA\n"
|
"Language: uk_UA\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "Особисті дані"
|
msgstr "Особисті дані"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "Дозволи"
|
msgstr "Дозволи"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "Важливі дати"
|
msgstr "Важливі дати"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "Ієрархічна структура"
|
msgstr "Ієрархічна структура"
|
||||||
|
|
||||||
@@ -50,27 +50,36 @@ msgstr "Приховано"
|
|||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "Обране"
|
msgstr "Обране"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "Новий документ був створений від вашого імені!"
|
msgstr "Новий документ був створений від вашого імені!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "Ви тепер є власником нового документа:"
|
msgstr "Ви тепер є власником нового документа:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr "Це поле є обов’язковим."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "Вміст"
|
msgstr "Вміст"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "Тип вмісту"
|
msgstr "Тип вмісту"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Формат"
|
msgstr "Формат"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "копія {title}"
|
msgstr "копія {title}"
|
||||||
@@ -169,247 +178,247 @@ msgstr "Обов'язкове. 255 символів або менше. Тіль
|
|||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "повне ім'я"
|
msgstr "повне ім'я"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "коротке ім'я"
|
msgstr "коротке ім'я"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "адреса електронної пошти особи"
|
msgstr "адреса електронної пошти особи"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "електронна адреса адміністратора"
|
msgstr "електронна адреса адміністратора"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "мова"
|
msgstr "мова"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "Мова, якою користувач хоче бачити інтерфейс."
|
msgstr "Мова, якою користувач хоче бачити інтерфейс."
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Часовий пояс, в якому користувач хоче бачити час."
|
msgstr "Часовий пояс, в якому користувач хоче бачити час."
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "пристрій"
|
msgstr "пристрій"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Чи є користувач пристроєм чи реальним користувачем."
|
msgstr "Чи є користувач пристроєм чи реальним користувачем."
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "статус співробітника"
|
msgstr "статус співробітника"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Чи може користувач увійти на цей сайт адміністратора."
|
msgstr "Чи може користувач увійти на цей сайт адміністратора."
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "активний"
|
msgstr "активний"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
|
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "користувач"
|
msgstr "користувач"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "користувачі"
|
msgstr "користувачі"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "заголовок"
|
msgstr "заголовок"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "уривок"
|
msgstr "уривок"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "Документ"
|
msgstr "Документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "Документи"
|
msgstr "Документи"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Документ без назви"
|
msgstr "Документ без назви"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} ділиться з вами документом!"
|
msgstr "{name} ділиться з вами документом!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} ділиться з вами документом: {title}"
|
msgstr "{name} ділиться з вами документом: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "Трасування посилання Документ/користувач"
|
msgstr "Трасування посилання Документ/користувач"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "Трасування посилань Документ/користувач"
|
msgstr "Трасування посилань Документ/користувач"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
|
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Обраний документ"
|
msgstr "Обраний документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Обрані документи"
|
msgstr "Обрані документи"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr "Цей документ вже вказаний як обраний для одного користувача."
|
msgstr "Цей документ вже вказаний як обраний для одного користувача."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "Відносини документ/користувач"
|
msgstr "Відносини документ/користувач"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "Відносини документ/користувач"
|
msgstr "Відносини документ/користувач"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "Цей користувач вже має доступ до цього документу."
|
msgstr "Цей користувач вже має доступ до цього документу."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "Ця команда вже має доступ до цього документа."
|
msgstr "Ця команда вже має доступ до цього документа."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Вкажіть користувача або команду, а не обох."
|
msgstr "Вкажіть користувача або команду, а не обох."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr "Запит доступу до документа"
|
msgstr "Запит доступу до документа"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr "Запит доступу для документа"
|
msgstr "Запит доступу для документа"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr "Цей користувач вже попросив доступ до цього документа."
|
msgstr "Цей користувач вже попросив доступ до цього документа."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr "{name} хоче отримати доступ до документа!"
|
msgstr "{name} хоче отримати доступ до документа!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr "{name} бажає отримати доступ до наступного документа:"
|
msgstr "{name} бажає отримати доступ до наступного документа:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr "{name} запитує доступ до документа: {title}"
|
msgstr "{name} запитує доступ до документа: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "опис"
|
msgstr "опис"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "код"
|
msgstr "код"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "публічне"
|
msgstr "публічне"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
|
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Шаблон"
|
msgstr "Шаблон"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Шаблони"
|
msgstr "Шаблони"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "Відношення шаблон/користувач"
|
msgstr "Відношення шаблон/користувач"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "Відношення шаблон/користувач"
|
msgstr "Відношення шаблон/користувач"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "Цей користувач вже має доступ до цього шаблону."
|
msgstr "Цей користувач вже має доступ до цього шаблону."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "Ця команда вже має доступ до цього шаблону."
|
msgstr "Ця команда вже має доступ до цього шаблону."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "електронна адреса"
|
msgstr "електронна адреса"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Запрошення до редагування документа"
|
msgstr "Запрошення до редагування документа"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Запрошення до редагування документів"
|
msgstr "Запрошення до редагування документів"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Логотип пошти"
|
msgstr "Логотип пошти"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Відкрити"
|
msgstr "Відкрити"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr " Docs, ваш новий важливий інструмент для організації, обміну та командної співпраці над вашими документами. "
|
msgstr " Docs, ваш новий важливий інструмент для організації, обміну та командної співпраці над вашими документами. "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: lasuite-docs\n"
|
"Project-Id-Version: lasuite-docs\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
"PO-Revision-Date: 2025-11-19 10:13\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Chinese Simplified\n"
|
"Language-Team: Chinese Simplified\n"
|
||||||
"Language: zh_CN\n"
|
"Language: zh_CN\n"
|
||||||
@@ -17,20 +17,20 @@ msgstr ""
|
|||||||
"X-Crowdin-File: backend-impress.pot\n"
|
"X-Crowdin-File: backend-impress.pot\n"
|
||||||
"X-Crowdin-File-ID: 18\n"
|
"X-Crowdin-File-ID: 18\n"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:37 core/admin.py:37
|
#: build/lib/core/admin.py:36 core/admin.py:36
|
||||||
msgid "Personal info"
|
msgid "Personal info"
|
||||||
msgstr "个人信息"
|
msgstr "个人信息"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
|
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
|
||||||
#: core/admin.py:138
|
#: core/admin.py:137
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr "权限"
|
msgstr "权限"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
#: build/lib/core/admin.py:61 core/admin.py:61
|
||||||
msgid "Important dates"
|
msgid "Important dates"
|
||||||
msgstr "重要日期"
|
msgstr "重要日期"
|
||||||
|
|
||||||
#: build/lib/core/admin.py:148 core/admin.py:148
|
#: build/lib/core/admin.py:147 core/admin.py:147
|
||||||
msgid "Tree structure"
|
msgid "Tree structure"
|
||||||
msgstr "树状结构"
|
msgstr "树状结构"
|
||||||
|
|
||||||
@@ -44,33 +44,42 @@ msgstr "创建者是我"
|
|||||||
|
|
||||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||||
msgid "Masked"
|
msgid "Masked"
|
||||||
msgstr ""
|
msgstr "已屏蔽"
|
||||||
|
|
||||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||||
msgid "Favorite"
|
msgid "Favorite"
|
||||||
msgstr "收藏"
|
msgstr "收藏"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||||
msgid "A new document was created on your behalf!"
|
msgid "A new document was created on your behalf!"
|
||||||
msgstr "已为您创建了一份新文档!"
|
msgstr "已为您创建了一份新文档!"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||||
msgid "You have been granted ownership of a new document:"
|
msgid "You have been granted ownership of a new document:"
|
||||||
msgstr "您已被授予新文档的所有权:"
|
msgstr "您已被授予新文档的所有权:"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
|
msgid "This field is required."
|
||||||
|
msgstr "必填字段。"
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
|
||||||
|
#, python-format
|
||||||
|
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
|
||||||
msgid "Body"
|
msgid "Body"
|
||||||
msgstr "正文"
|
msgstr "正文"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||||
msgid "Body type"
|
msgid "Body type"
|
||||||
msgstr "正文类型"
|
msgstr "正文类型"
|
||||||
|
|
||||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "格式"
|
msgstr "格式"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "copy of {title}"
|
msgid "copy of {title}"
|
||||||
msgstr "{title} 的副本"
|
msgstr "{title} 的副本"
|
||||||
@@ -163,253 +172,253 @@ msgstr "sub"
|
|||||||
|
|
||||||
#: build/lib/core/models.py:142 core/models.py:142
|
#: build/lib/core/models.py:142 core/models.py:142
|
||||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||||
msgstr ""
|
msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:150 core/models.py:150
|
#: build/lib/core/models.py:150 core/models.py:150
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "全名"
|
msgstr "全名"
|
||||||
|
|
||||||
#: build/lib/core/models.py:151 core/models.py:151
|
#: build/lib/core/models.py:152 core/models.py:152
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "简称"
|
msgstr "简称"
|
||||||
|
|
||||||
#: build/lib/core/models.py:153 core/models.py:153
|
#: build/lib/core/models.py:155 core/models.py:155
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "身份电子邮件地址"
|
msgstr "身份电子邮件地址"
|
||||||
|
|
||||||
#: build/lib/core/models.py:158 core/models.py:158
|
#: build/lib/core/models.py:160 core/models.py:160
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "管理员电子邮件地址"
|
msgstr "管理员电子邮件地址"
|
||||||
|
|
||||||
#: build/lib/core/models.py:165 core/models.py:165
|
#: build/lib/core/models.py:167 core/models.py:167
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "语言"
|
msgstr "语言"
|
||||||
|
|
||||||
#: build/lib/core/models.py:166 core/models.py:166
|
#: build/lib/core/models.py:168 core/models.py:168
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "用户希望看到的界面语言。"
|
msgstr "用户希望看到的界面语言。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:174 core/models.py:174
|
#: build/lib/core/models.py:176 core/models.py:176
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "用户查看时间希望的时区。"
|
msgstr "用户查看时间希望的时区。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:177 core/models.py:177
|
#: build/lib/core/models.py:179 core/models.py:179
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "设备"
|
msgstr "设备"
|
||||||
|
|
||||||
#: build/lib/core/models.py:179 core/models.py:179
|
#: build/lib/core/models.py:181 core/models.py:181
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "用户是设备还是真实用户。"
|
msgstr "用户是设备还是真实用户。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:182 core/models.py:182
|
#: build/lib/core/models.py:184 core/models.py:184
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "员工状态"
|
msgstr "员工状态"
|
||||||
|
|
||||||
#: build/lib/core/models.py:184 core/models.py:184
|
#: build/lib/core/models.py:186 core/models.py:186
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "用户是否可以登录该管理员站点。"
|
msgstr "用户是否可以登录该管理员站点。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:187 core/models.py:187
|
#: build/lib/core/models.py:189 core/models.py:189
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "激活"
|
msgstr "激活"
|
||||||
|
|
||||||
#: build/lib/core/models.py:190 core/models.py:190
|
#: build/lib/core/models.py:192 core/models.py:192
|
||||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||||
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
|
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:202 core/models.py:202
|
#: build/lib/core/models.py:204 core/models.py:204
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "用户"
|
msgstr "用户"
|
||||||
|
|
||||||
#: build/lib/core/models.py:203 core/models.py:203
|
#: build/lib/core/models.py:205 core/models.py:205
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "个用户"
|
msgstr "个用户"
|
||||||
|
|
||||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1281
|
#: core/models.py:361 core/models.py:1276
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "标题"
|
msgstr "标题"
|
||||||
|
|
||||||
#: build/lib/core/models.py:360 core/models.py:360
|
#: build/lib/core/models.py:362 core/models.py:362
|
||||||
msgid "excerpt"
|
msgid "excerpt"
|
||||||
msgstr "摘要"
|
msgstr "摘要"
|
||||||
|
|
||||||
#: build/lib/core/models.py:409 core/models.py:409
|
#: build/lib/core/models.py:411 core/models.py:411
|
||||||
msgid "Document"
|
msgid "Document"
|
||||||
msgstr "文档"
|
msgstr "文档"
|
||||||
|
|
||||||
#: build/lib/core/models.py:410 core/models.py:410
|
#: build/lib/core/models.py:412 core/models.py:412
|
||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr "个文档"
|
msgstr "个文档"
|
||||||
|
|
||||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:819
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "未命名文档"
|
msgstr "未命名文档"
|
||||||
|
|
||||||
#: build/lib/core/models.py:854 core/models.py:854
|
#: build/lib/core/models.py:857 core/models.py:857
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you!"
|
msgid "{name} shared a document with you!"
|
||||||
msgstr "{name} 与您共享了一个文档!"
|
msgstr "{name} 与您共享了一个文档!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:858 core/models.py:858
|
#: build/lib/core/models.py:861 core/models.py:861
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||||
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:864 core/models.py:864
|
#: build/lib/core/models.py:867 core/models.py:867
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} shared a document with you: {title}"
|
msgid "{name} shared a document with you: {title}"
|
||||||
msgstr "{name} 与您共享了一个文档:{title}"
|
msgstr "{name} 与您共享了一个文档:{title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:964 core/models.py:964
|
#: build/lib/core/models.py:967 core/models.py:967
|
||||||
msgid "Document/user link trace"
|
msgid "Document/user link trace"
|
||||||
msgstr "文档/用户链接跟踪"
|
msgstr "文档/用户链接跟踪"
|
||||||
|
|
||||||
#: build/lib/core/models.py:965 core/models.py:965
|
#: build/lib/core/models.py:968 core/models.py:968
|
||||||
msgid "Document/user link traces"
|
msgid "Document/user link traces"
|
||||||
msgstr "个文档/用户链接跟踪"
|
msgstr "个文档/用户链接跟踪"
|
||||||
|
|
||||||
#: build/lib/core/models.py:971 core/models.py:971
|
#: build/lib/core/models.py:974 core/models.py:974
|
||||||
msgid "A link trace already exists for this document/user."
|
msgid "A link trace already exists for this document/user."
|
||||||
msgstr "此文档/用户的链接跟踪已存在。"
|
msgstr "此文档/用户的链接跟踪已存在。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:994 core/models.py:994
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "文档收藏"
|
msgstr "文档收藏"
|
||||||
|
|
||||||
#: build/lib/core/models.py:995 core/models.py:995
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "文档收藏夹"
|
msgstr "文档收藏夹"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
#: build/lib/core/models.py:1004 core/models.py:1004
|
||||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||||
msgstr "该文档已被同一用户的收藏关系实例关联。"
|
msgstr "该文档已被同一用户的收藏关系实例关联。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
#: build/lib/core/models.py:1026 core/models.py:1026
|
||||||
msgid "Document/user relation"
|
msgid "Document/user relation"
|
||||||
msgstr "文档/用户关系"
|
msgstr "文档/用户关系"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
#: build/lib/core/models.py:1027 core/models.py:1027
|
||||||
msgid "Document/user relations"
|
msgid "Document/user relations"
|
||||||
msgstr "文档/用户关系集"
|
msgstr "文档/用户关系集"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
#: build/lib/core/models.py:1033 core/models.py:1033
|
||||||
msgid "This user is already in this document."
|
msgid "This user is already in this document."
|
||||||
msgstr "该用户已在此文档中。"
|
msgstr "该用户已在此文档中。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
#: build/lib/core/models.py:1039 core/models.py:1039
|
||||||
msgid "This team is already in this document."
|
msgid "This team is already in this document."
|
||||||
msgstr "该团队已在此文档中。"
|
msgstr "该团队已在此文档中。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1042 core/models.py:1367
|
#: core/models.py:1045 core/models.py:1362
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "必须设置用户或团队之一,不能同时设置两者。"
|
msgstr "必须设置用户或团队之一,不能同时设置两者。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr ""
|
msgstr "文档需要访问权限"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr ""
|
msgstr "文档需要访问权限"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
#: build/lib/core/models.py:1198 core/models.py:1198
|
||||||
msgid "This user has already asked for access to this document."
|
msgid "This user has already asked for access to this document."
|
||||||
msgstr ""
|
msgstr "用户已申请该文档的访问权限。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
#: build/lib/core/models.py:1255 core/models.py:1255
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to a document!"
|
msgid "{name} would like access to a document!"
|
||||||
msgstr ""
|
msgstr "{name} 申请访问文档!"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
msgid "{name} would like access to the following document:"
|
||||||
msgstr ""
|
msgstr "{name} 申请访问以下文档:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} is asking for access to the document: {title}"
|
msgid "{name} is asking for access to the document: {title}"
|
||||||
msgstr ""
|
msgstr "{name}申请文档:{title}的访问权限"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "说明"
|
msgstr "说明"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "代码"
|
msgstr "代码"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "公开"
|
msgstr "公开"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||||
msgid "Whether this template is public for anyone to use."
|
msgid "Whether this template is public for anyone to use."
|
||||||
msgstr "该模板是否公开供任何人使用。"
|
msgstr "该模板是否公开供任何人使用。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "模板"
|
msgstr "模板"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "模板"
|
msgstr "模板"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
#: build/lib/core/models.py:1343 core/models.py:1343
|
||||||
msgid "Template/user relation"
|
msgid "Template/user relation"
|
||||||
msgstr "模板/用户关系"
|
msgstr "模板/用户关系"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: build/lib/core/models.py:1344 core/models.py:1344
|
||||||
msgid "Template/user relations"
|
msgid "Template/user relations"
|
||||||
msgstr "模板/用户关系集"
|
msgstr "模板/用户关系集"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "This user is already in this template."
|
msgid "This user is already in this template."
|
||||||
msgstr "该用户已在此模板中。"
|
msgstr "该用户已在此模板中。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This team is already in this template."
|
msgid "This team is already in this template."
|
||||||
msgstr "该团队已在此模板中。"
|
msgstr "该团队已在此模板中。"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "电子邮件地址"
|
msgstr "电子邮件地址"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "文档邀请"
|
msgstr "文档邀请"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "文档邀请"
|
msgstr "文档邀请"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
#: build/lib/core/models.py:1473 core/models.py:1473
|
||||||
msgid "This email is already associated to a registered user."
|
msgid "This email is already associated to a registered user."
|
||||||
msgstr "此电子邮件已经与现有注册用户关联。"
|
msgstr "此电子邮件已经与现有注册用户关联。"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:162
|
#: core/templates/mail/html/template.html:153
|
||||||
#: core/templates/mail/text/template.txt:3
|
#: core/templates/mail/text/template.txt:3
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "徽标邮件"
|
msgstr "徽标邮件"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:209
|
#: core/templates/mail/html/template.html:200
|
||||||
#: core/templates/mail/text/template.txt:10
|
#: core/templates/mail/text/template.txt:10
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "打开"
|
msgstr "打开"
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:226
|
#: core/templates/mail/html/template.html:217
|
||||||
#: core/templates/mail/text/template.txt:14
|
#: core/templates/mail/text/template.txt:14
|
||||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||||
msgstr " Docs——您的全新必备工具,帮助团队组织、共享和协作处理文档。 "
|
msgstr " Docs——您的全新必备工具,帮助团队组织、共享和协作处理文档。 "
|
||||||
|
|
||||||
#: core/templates/mail/html/template.html:233
|
#: core/templates/mail/html/template.html:224
|
||||||
#: core/templates/mail/text/template.txt:16
|
#: core/templates/mail/text/template.txt:16
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Brought to you by %(brandname)s "
|
msgid " Brought to you by %(brandname)s "
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "impress"
|
name = "impress"
|
||||||
version = "3.7.0"
|
version = "3.10.0"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
@@ -25,42 +25,42 @@ license = { file = "LICENSE" }
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"beautifulsoup4==4.13.4",
|
"beautifulsoup4==4.14.2",
|
||||||
"boto3==1.39.4",
|
"boto3==1.40.74",
|
||||||
"Brotli==1.1.0",
|
"Brotli==1.2.0",
|
||||||
"celery[redis]==5.5.3",
|
"celery[redis]==5.5.3",
|
||||||
"django-configurations==2.5.1",
|
"django-configurations==2.5.1",
|
||||||
"django-cors-headers==4.7.0",
|
"django-cors-headers==4.9.0",
|
||||||
"django-countries==7.6.1",
|
"django-countries==8.1.0",
|
||||||
"django-csp==4.0",
|
"django-csp==4.0",
|
||||||
"django-filter==25.1",
|
"django-filter==25.2",
|
||||||
"django-lasuite[all]==0.0.14",
|
"django-lasuite[all]==0.0.18",
|
||||||
"django-parler==2.3",
|
"django-parler==2.3",
|
||||||
"django-redis==6.0.0",
|
"django-redis==6.0.0",
|
||||||
"django-storages[s3]==1.14.6",
|
"django-storages[s3]==1.14.6",
|
||||||
"django-timezone-field>=5.1",
|
"django-timezone-field>=5.1",
|
||||||
"django==5.2.7",
|
"django==5.2.8",
|
||||||
"django-treebeard==4.7.1",
|
"django-treebeard==4.7.1",
|
||||||
"djangorestframework==3.16.0",
|
"djangorestframework==3.16.1",
|
||||||
"drf_spectacular==0.28.0",
|
"drf_spectacular==0.29.0",
|
||||||
"dockerflow==2024.4.2",
|
"dockerflow==2024.4.2",
|
||||||
"easy_thumbnails==2.10",
|
"easy_thumbnails==2.10.1",
|
||||||
"factory_boy==3.3.3",
|
"factory_boy==3.3.3",
|
||||||
"gunicorn==23.0.0",
|
"gunicorn==23.0.0",
|
||||||
"jsonschema==4.24.0",
|
"jsonschema==4.25.1",
|
||||||
"lxml==6.0.0",
|
"lxml==6.0.2",
|
||||||
"markdown==3.8.2",
|
"markdown==3.10",
|
||||||
"mozilla-django-oidc==4.0.1",
|
"mozilla-django-oidc==4.0.1",
|
||||||
"nested-multipart-parser==1.5.0",
|
"nested-multipart-parser==1.6.0",
|
||||||
"openai==1.95.0",
|
"openai==2.8.0",
|
||||||
"psycopg[binary]==3.2.9",
|
"psycopg[binary]==3.2.12",
|
||||||
"pycrdt==0.12.25",
|
"pycrdt==0.12.43",
|
||||||
"PyJWT==2.10.1",
|
"PyJWT==2.10.1",
|
||||||
"python-magic==0.4.27",
|
"python-magic==0.4.27",
|
||||||
"redis<6.0.0",
|
"redis<6.0.0",
|
||||||
"requests==2.32.4",
|
"requests==2.32.5",
|
||||||
"sentry-sdk==2.32.0",
|
"sentry-sdk==2.44.0",
|
||||||
"whitenoise==6.9.0",
|
"whitenoise==6.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -73,21 +73,21 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
"django-extensions==4.1",
|
"django-extensions==4.1",
|
||||||
"django-test-migrations==1.5.0",
|
"django-test-migrations==1.5.0",
|
||||||
"drf-spectacular-sidecar==2025.7.1",
|
"drf-spectacular-sidecar==2025.10.1",
|
||||||
"freezegun==1.5.2",
|
"freezegun==1.5.5",
|
||||||
"ipdb==0.13.13",
|
"ipdb==0.13.13",
|
||||||
"ipython==9.4.0",
|
"ipython==9.7.0",
|
||||||
"pyfakefs==5.9.1",
|
"pyfakefs==5.10.2",
|
||||||
"pylint-django==2.6.1",
|
"pylint-django==2.6.1",
|
||||||
"pylint==3.3.7",
|
"pylint<4.0.0",
|
||||||
"pytest-cov==6.2.1",
|
"pytest-cov==7.0.0",
|
||||||
"pytest-django==4.11.1",
|
"pytest-django==4.11.1",
|
||||||
"pytest==8.4.1",
|
"pytest==9.0.1",
|
||||||
"pytest-icdiff==0.9",
|
"pytest-icdiff==0.9",
|
||||||
"pytest-xdist==3.8.0",
|
"pytest-xdist==3.8.0",
|
||||||
"responses==0.25.7",
|
"responses==0.25.8",
|
||||||
"ruff==0.12.2",
|
"ruff==0.14.5",
|
||||||
"types-requests==2.32.4.20250611",
|
"types-requests==2.32.4.20250913",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
|
|||||||
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panojpg.jpeg
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panopng.png
Normal file
|
After Width: | Height: | Size: 839 KiB |
295
src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { createDoc, getOtherBrowserName, verifyDocName } from './utils-common';
|
||||||
|
import { writeInEditor } from './utils-editor';
|
||||||
|
import {
|
||||||
|
addNewMember,
|
||||||
|
connectOtherUserToDoc,
|
||||||
|
updateRoleUser,
|
||||||
|
updateShareLink,
|
||||||
|
} from './utils-share';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Doc Comments', () => {
|
||||||
|
test('it checks comments with 2 users in real time', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
|
||||||
|
|
||||||
|
// We share the doc with another user
|
||||||
|
const otherBrowserName = getOtherBrowserName(browserName);
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await addNewMember(page, 0, 'Administrator', otherBrowserName);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByRole('listbox', { name: 'Suggestions' })
|
||||||
|
.getByText(new RegExp(otherBrowserName)),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'close' }).click();
|
||||||
|
|
||||||
|
// We add a comment with the first user
|
||||||
|
const editor = await writeInEditor({ page, text: 'Hello World' });
|
||||||
|
await editor.getByText('Hello').selectText();
|
||||||
|
await page.getByRole('button', { name: 'Comment' }).click();
|
||||||
|
|
||||||
|
const thread = page.locator('.bn-thread');
|
||||||
|
await thread.getByRole('paragraph').first().fill('This is a comment');
|
||||||
|
await thread.locator('[data-test="save"]').click();
|
||||||
|
await expect(thread.getByText('This is a comment').first()).toBeHidden();
|
||||||
|
|
||||||
|
await editor.getByText('Hello').click();
|
||||||
|
|
||||||
|
await thread.getByText('This is a comment').first().hover();
|
||||||
|
|
||||||
|
// We add a reaction with the first user
|
||||||
|
await thread.locator('[data-test="addreaction"]').first().click();
|
||||||
|
await page.getByRole('button', { name: '👍' }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
thread.getByRole('img', { name: 'E2E Chromium' }).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(thread.getByText('This is a comment').first()).toBeVisible();
|
||||||
|
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
|
||||||
|
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
|
||||||
|
|
||||||
|
const urlCommentDoc = page.url();
|
||||||
|
|
||||||
|
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||||
|
otherBrowserName,
|
||||||
|
docUrl: urlCommentDoc,
|
||||||
|
docTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherEditor = otherPage.locator('.ProseMirror');
|
||||||
|
await otherEditor.getByText('Hello').click();
|
||||||
|
const otherThread = otherPage.locator('.bn-thread');
|
||||||
|
|
||||||
|
await otherThread.getByText('This is a comment').first().hover();
|
||||||
|
await otherThread.locator('[data-test="addreaction"]').first().click();
|
||||||
|
await otherPage.getByRole('button', { name: '👍' }).click();
|
||||||
|
|
||||||
|
// We check that the comment made by the first user is visible for the second user
|
||||||
|
await expect(
|
||||||
|
otherThread.getByText('This is a comment').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
otherThread.getByText(`E2E ${browserName}`).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(otherThread.locator('.bn-comment-reaction')).toHaveText('👍2');
|
||||||
|
|
||||||
|
// We add a comment with the second user
|
||||||
|
await otherThread
|
||||||
|
.getByRole('paragraph')
|
||||||
|
.last()
|
||||||
|
.fill('This is a comment from the other user');
|
||||||
|
await otherThread.locator('[data-test="save"]').click();
|
||||||
|
|
||||||
|
// We check that the second user can see the comment he just made
|
||||||
|
await expect(
|
||||||
|
otherThread.getByRole('img', { name: `E2E ${otherBrowserName}` }).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
otherThread.getByText('This is a comment from the other user').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
otherThread.getByText(`E2E ${otherBrowserName}`).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// We check that the first user can see the comment made by the second user in real time
|
||||||
|
await expect(
|
||||||
|
thread.getByText('This is a comment from the other user').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
thread.getByText(`E2E ${otherBrowserName}`).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks the comments interactions', async ({ page, browserName }) => {
|
||||||
|
await createDoc(page, 'comment-interaction', browserName, 1);
|
||||||
|
|
||||||
|
// Checks add react reaction
|
||||||
|
const editor = page.locator('.ProseMirror');
|
||||||
|
await editor.locator('.bn-block-outer').last().fill('Hello World');
|
||||||
|
await editor.getByText('Hello').selectText();
|
||||||
|
await page.getByRole('button', { name: 'Comment' }).click();
|
||||||
|
|
||||||
|
const thread = page.locator('.bn-thread');
|
||||||
|
await thread.getByRole('paragraph').first().fill('This is a comment');
|
||||||
|
await thread.locator('[data-test="save"]').click();
|
||||||
|
await expect(thread.getByText('This is a comment').first()).toBeHidden();
|
||||||
|
|
||||||
|
// Check background color changed
|
||||||
|
await expect(editor.getByText('Hello')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgba(237, 180, 0, 0.4)',
|
||||||
|
);
|
||||||
|
await editor.getByText('Hello').click();
|
||||||
|
|
||||||
|
await thread.getByText('This is a comment').first().hover();
|
||||||
|
|
||||||
|
// We add a reaction with the first user
|
||||||
|
await thread.locator('[data-test="addreaction"]').first().click();
|
||||||
|
await page.getByRole('button', { name: '👍' }).click();
|
||||||
|
|
||||||
|
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
|
||||||
|
|
||||||
|
// Edit Comment
|
||||||
|
await thread.getByText('This is a comment').first().hover();
|
||||||
|
await thread.locator('[data-test="moreactions"]').first().click();
|
||||||
|
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
|
||||||
|
const commentEditor = thread.getByText('This is a comment').first();
|
||||||
|
await commentEditor.fill('This is an edited comment');
|
||||||
|
const saveBtn = thread.getByRole('button', { name: 'Save' });
|
||||||
|
await saveBtn.click();
|
||||||
|
await expect(saveBtn).toBeHidden();
|
||||||
|
await expect(
|
||||||
|
thread.getByText('This is an edited comment').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(thread.getByText('This is a comment').first()).toBeHidden();
|
||||||
|
|
||||||
|
// Add second comment
|
||||||
|
await thread.getByRole('paragraph').last().fill('This is a second comment');
|
||||||
|
await thread.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await expect(
|
||||||
|
thread.getByText('This is an edited comment').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
thread.getByText('This is a second comment').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Delete second comment
|
||||||
|
await thread.getByText('This is a second comment').first().hover();
|
||||||
|
await thread.locator('[data-test="moreactions"]').first().click();
|
||||||
|
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
|
||||||
|
await expect(
|
||||||
|
thread.getByText('This is a second comment').first(),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
// Resolve thread
|
||||||
|
await thread.getByText('This is an edited comment').first().hover();
|
||||||
|
await thread.locator('[data-test="resolve"]').click();
|
||||||
|
await expect(thread).toBeHidden();
|
||||||
|
await expect(editor.getByText('Hello')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgba(0, 0, 0, 0)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks the comments abilities', async ({ page, browserName }) => {
|
||||||
|
test.slow();
|
||||||
|
|
||||||
|
const [docTitle] = await createDoc(page, 'comment-doc', browserName, 1);
|
||||||
|
|
||||||
|
// We share the doc with another user
|
||||||
|
const otherBrowserName = getOtherBrowserName(browserName);
|
||||||
|
|
||||||
|
// Add a new member with editor role
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await addNewMember(page, 0, 'Editor', otherBrowserName);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByRole('listbox', { name: 'Suggestions' })
|
||||||
|
.getByText(new RegExp(otherBrowserName)),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const urlCommentDoc = page.url();
|
||||||
|
|
||||||
|
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||||
|
otherBrowserName,
|
||||||
|
docUrl: urlCommentDoc,
|
||||||
|
docTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherEditor = await writeInEditor({
|
||||||
|
page: otherPage,
|
||||||
|
text: 'Hello, I can edit the document',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
otherEditor.getByText('Hello, I can edit the document'),
|
||||||
|
).toBeVisible();
|
||||||
|
await otherEditor.getByText('Hello').selectText();
|
||||||
|
await otherPage.getByRole('button', { name: 'Comment' }).click();
|
||||||
|
const otherThread = otherPage.locator('.bn-thread');
|
||||||
|
await otherThread
|
||||||
|
.getByRole('paragraph')
|
||||||
|
.first()
|
||||||
|
.fill('I can add a comment');
|
||||||
|
await otherThread.locator('[data-test="save"]').click();
|
||||||
|
await expect(
|
||||||
|
otherThread.getByText('I can add a comment').first(),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
await expect(otherEditor.getByText('Hello')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgba(237, 180, 0, 0.4)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// We change the role of the second user to reader
|
||||||
|
await updateRoleUser(page, 'Reader', `user.test@${otherBrowserName}.test`);
|
||||||
|
|
||||||
|
// With the reader role, the second user cannot see comments
|
||||||
|
await otherPage.reload();
|
||||||
|
await verifyDocName(otherPage, docTitle);
|
||||||
|
|
||||||
|
await expect(otherEditor.getByText('Hello')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgba(0, 0, 0, 0)',
|
||||||
|
);
|
||||||
|
await otherEditor.getByText('Hello').click();
|
||||||
|
await expect(otherThread).toBeHidden();
|
||||||
|
await otherEditor.getByText('Hello').selectText();
|
||||||
|
await expect(
|
||||||
|
otherPage.getByRole('button', { name: 'Comment' }),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
await otherPage.reload();
|
||||||
|
|
||||||
|
// Change the link role of the doc to set it in commenting mode
|
||||||
|
await updateShareLink(page, 'Public', 'Editing');
|
||||||
|
|
||||||
|
// Anonymous user can see and add comments
|
||||||
|
await otherPage.getByRole('button', { name: 'Logout' }).click();
|
||||||
|
|
||||||
|
await otherPage.goto(urlCommentDoc);
|
||||||
|
|
||||||
|
await verifyDocName(otherPage, docTitle);
|
||||||
|
|
||||||
|
await expect(otherEditor.getByText('Hello')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgba(237, 180, 0, 0.4)',
|
||||||
|
);
|
||||||
|
await otherEditor.getByText('Hello').click();
|
||||||
|
await expect(
|
||||||
|
otherThread.getByText('I can add a comment').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await otherThread
|
||||||
|
.locator('.ProseMirror.bn-editor[contenteditable="true"]')
|
||||||
|
.getByRole('paragraph')
|
||||||
|
.first()
|
||||||
|
.fill('Comment by anonymous user');
|
||||||
|
await otherThread.locator('[data-test="save"]').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
otherThread.getByText('Comment by anonymous user').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
otherThread.getByRole('img', { name: `Anonymous` }).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await otherThread.getByText('Comment by anonymous user').first().hover();
|
||||||
|
await expect(otherThread.locator('[data-test="moreactions"]')).toBeHidden();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,22 +13,24 @@ import {
|
|||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
|
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||||
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
|
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
|
||||||
import { createRootSubPage, navigateToPageFromTree } from './utils-sub-pages';
|
import {
|
||||||
|
createRootSubPage,
|
||||||
|
getTreeRow,
|
||||||
|
navigateToPageFromTree,
|
||||||
|
} from './utils-sub-pages';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Editor', () => {
|
test.describe('Doc Editor', () => {
|
||||||
test('it checks default toolbar buttons are displayed', async ({
|
test('it checks toolbar buttons are displayed', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = await writeInEditor({ page, text: 'test content' });
|
||||||
await editor.click();
|
|
||||||
await editor.fill('test content');
|
|
||||||
|
|
||||||
await editor
|
await editor
|
||||||
.getByText('test content', {
|
.getByText('test content', {
|
||||||
@@ -37,6 +39,9 @@ test.describe('Doc Editor', () => {
|
|||||||
.selectText();
|
.selectText();
|
||||||
|
|
||||||
const toolbar = page.locator('.bn-formatting-toolbar');
|
const toolbar = page.locator('.bn-formatting-toolbar');
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="comment-toolbar-button"]'),
|
||||||
|
).toBeVisible();
|
||||||
await expect(toolbar.locator('button[data-test="bold"]')).toBeVisible();
|
await expect(toolbar.locator('button[data-test="bold"]')).toBeVisible();
|
||||||
await expect(toolbar.locator('button[data-test="italic"]')).toBeVisible();
|
await expect(toolbar.locator('button[data-test="italic"]')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
@@ -59,6 +64,53 @@ test.describe('Doc Editor', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
toolbar.locator('button[data-test="createLink"]'),
|
toolbar.locator('button[data-test="createLink"]'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="ai-actions"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="convertMarkdown"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await page.locator('.bn-block-outer').last().click();
|
||||||
|
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
|
await page.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await page.getByText('Resizable image with caption').click();
|
||||||
|
await page.getByText('Upload image').click();
|
||||||
|
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(
|
||||||
|
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = page
|
||||||
|
.locator('.--docs--editor-container img.bn-visual-media')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
await expect(image).toHaveAttribute('role', 'presentation');
|
||||||
|
|
||||||
|
await image.dblclick();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="comment-toolbar-button"]'),
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="ai-actions"]'),
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="convertMarkdown"]'),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="editcaption"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
toolbar.locator('button[data-test="downloadfile"]'),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,20 +289,66 @@ test.describe('Doc Editor', () => {
|
|||||||
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
|
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it cannot edit if viewer', async ({ page }) => {
|
test('it cannot edit if viewer but see and can get resources', async ({
|
||||||
await mockedDocument(page, {
|
page,
|
||||||
user_role: 'reader',
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [docTitle] = await createDoc(page, 'doc-viewer', browserName, 1);
|
||||||
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
|
await writeInEditor({ page, text: 'Hello World' });
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await updateShareLink(page, 'Public', 'Reading');
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
await page.getByRole('button', { name: 'close' }).first().click();
|
||||||
|
|
||||||
|
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||||
|
browserName,
|
||||||
|
docUrl: page.url(),
|
||||||
|
withoutSignIn: true,
|
||||||
|
docTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
await goToGridDoc(page);
|
await expect(
|
||||||
|
otherPage.getByLabel('It is the card information').getByText('Reader'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
const card = page.getByLabel('It is the card information');
|
// Cannot edit
|
||||||
await expect(card).toBeVisible();
|
const editor = otherPage.locator('.ProseMirror');
|
||||||
|
|
||||||
await expect(card.getByText('Reader')).toBeVisible();
|
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
|
||||||
await expect(editor).toHaveAttribute('contenteditable', 'false');
|
await expect(editor).toHaveAttribute('contenteditable', 'false');
|
||||||
|
|
||||||
|
// Owner add a image
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
|
await page.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await page.getByText('Resizable image with caption').click();
|
||||||
|
await page.getByText('Upload image').click();
|
||||||
|
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(
|
||||||
|
path.join(__dirname, 'assets/logo-suite-numerique.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Owner see the image
|
||||||
|
await expect(
|
||||||
|
page.locator('.--docs--editor-container img.bn-visual-media').first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Viewser see the image
|
||||||
|
const viewerImg = otherPage
|
||||||
|
.locator('.--docs--editor-container img.bn-visual-media')
|
||||||
|
.first();
|
||||||
|
await expect(viewerImg).toBeVisible();
|
||||||
|
|
||||||
|
// Viewer can download the image
|
||||||
|
await viewerImg.click();
|
||||||
|
const downloadPromise = otherPage.waitForEvent('download');
|
||||||
|
await otherPage.getByRole('button', { name: 'Download image' }).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toBe('logo-suite-numerique.png');
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it adds an image to the doc editor', async ({ page, browserName }) => {
|
test('it adds an image to the doc editor', async ({ page, browserName }) => {
|
||||||
@@ -490,7 +588,7 @@ test.describe('Doc Editor', () => {
|
|||||||
if (request.method().includes('GET')) {
|
if (request.method().includes('GET')) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
json: {
|
json: {
|
||||||
status: requestCount ? 'ready' : 'processing',
|
status: requestCount > 1 ? 'ready' : 'processing',
|
||||||
file: '/anything.html',
|
file: '/anything.html',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -514,6 +612,12 @@ test.describe('Doc Editor', () => {
|
|||||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
|
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
|
||||||
|
|
||||||
await expect(editor.getByText('Analyzing file...')).toBeVisible();
|
await expect(editor.getByText('Analyzing file...')).toBeVisible();
|
||||||
|
|
||||||
|
// To be sure the retry happens even after a page reload
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await expect(editor.getByText('Analyzing file...')).toBeVisible();
|
||||||
|
|
||||||
// The retry takes a few seconds
|
// The retry takes a few seconds
|
||||||
await expect(editor.getByText('test.html')).toBeVisible({
|
await expect(editor.getByText('test.html')).toBeVisible({
|
||||||
timeout: 7000,
|
timeout: 7000,
|
||||||
@@ -676,10 +780,9 @@ test.describe('Doc Editor', () => {
|
|||||||
|
|
||||||
await calloutBlock.locator('.inline-content').fill('example text');
|
await calloutBlock.locator('.inline-content').fill('example text');
|
||||||
|
|
||||||
await expect(page.locator('.bn-block').first()).toHaveAttribute(
|
await expect(
|
||||||
'data-background-color',
|
page.locator('.bn-block-content[data-content-type="callout"]').first(),
|
||||||
'yellow',
|
).toHaveAttribute('data-background-color', 'yellow');
|
||||||
);
|
|
||||||
|
|
||||||
const emojiButton = calloutBlock.getByRole('button');
|
const emojiButton = calloutBlock.getByRole('button');
|
||||||
await expect(emojiButton).toHaveText('💡');
|
await expect(emojiButton).toHaveText('💡');
|
||||||
@@ -703,10 +806,9 @@ test.describe('Doc Editor', () => {
|
|||||||
await page.locator('.mantine-Menu-dropdown > button').last().click();
|
await page.locator('.mantine-Menu-dropdown > button').last().click();
|
||||||
await page.locator('.bn-color-picker-dropdown > button').last().click();
|
await page.locator('.bn-color-picker-dropdown > button').last().click();
|
||||||
|
|
||||||
await expect(page.locator('.bn-block').first()).toHaveAttribute(
|
await expect(
|
||||||
'data-background-color',
|
page.locator('.bn-block-content[data-content-type="callout"]').first(),
|
||||||
'pink',
|
).toHaveAttribute('data-background-color', 'pink');
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks interlink feature', async ({ page, browserName }) => {
|
test('it checks interlink feature', async ({ page, browserName }) => {
|
||||||
@@ -730,7 +832,13 @@ test.describe('Doc Editor', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, docChild2);
|
await verifyDocName(page, docChild2);
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().fill('/');
|
const treeRow = await getTreeRow(page, docChild2);
|
||||||
|
await treeRow.locator('.--docs--doc-icon').click();
|
||||||
|
await page.getByRole('button', { name: '😀' }).first().click();
|
||||||
|
|
||||||
|
await navigateToPageFromTree({ page, title: docChild1 });
|
||||||
|
|
||||||
|
await openSuggestionMenu({ page });
|
||||||
await page.getByText('Link a doc').first().click();
|
await page.getByText('Link a doc').first().click();
|
||||||
|
|
||||||
const input = page.locator(
|
const input = page.locator(
|
||||||
@@ -744,6 +852,16 @@ test.describe('Doc Editor', () => {
|
|||||||
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
||||||
await expect(searchContainer.getByText(docChild2)).toBeVisible();
|
await expect(searchContainer.getByText(docChild2)).toBeVisible();
|
||||||
|
|
||||||
|
const searchContainerRow = searchContainer
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({
|
||||||
|
hasText: docChild2,
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
|
||||||
|
await expect(searchContainerRow).toContainText('😀');
|
||||||
|
await expect(searchContainerRow.locator('svg').first()).toBeHidden();
|
||||||
|
|
||||||
await input.pressSequentially('-child');
|
await input.pressSequentially('-child');
|
||||||
|
|
||||||
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
await expect(searchContainer.getByText(docChild1)).toBeVisible();
|
||||||
@@ -758,32 +876,36 @@ test.describe('Doc Editor', () => {
|
|||||||
await expect(searchContainer).toBeHidden();
|
await expect(searchContainer).toBeHidden();
|
||||||
|
|
||||||
// Wait for the interlink to be created and rendered
|
// Wait for the interlink to be created and rendered
|
||||||
const editor = page.locator('.ProseMirror.bn-editor');
|
const editor = await getEditor({ page });
|
||||||
|
|
||||||
const interlink = editor.getByRole('link', {
|
const interlinkChild2 = editor.getByRole('button', {
|
||||||
name: docChild2,
|
name: docChild2,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(interlink).toBeVisible({ timeout: 10000 });
|
await expect(interlinkChild2).toBeVisible({ timeout: 10000 });
|
||||||
await interlink.click();
|
await expect(interlinkChild2).toContainText('😀');
|
||||||
|
await expect(interlinkChild2.locator('svg').first()).toBeHidden();
|
||||||
|
await interlinkChild2.click();
|
||||||
|
|
||||||
await verifyDocName(page, docChild2);
|
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 editor.click();
|
||||||
|
|
||||||
|
await page.keyboard.press('@');
|
||||||
|
await input.fill(docChild1);
|
||||||
|
await searchContainer.getByText(docChild1).click();
|
||||||
|
|
||||||
|
const interlinkChild1 = editor.getByRole('button', {
|
||||||
|
name: docChild1,
|
||||||
|
});
|
||||||
|
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
|
||||||
|
|
||||||
await page.keyboard.press('@');
|
await page.keyboard.press('@');
|
||||||
|
|
||||||
await expect(
|
await page.keyboard.press('Escape');
|
||||||
page.locator(
|
|
||||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
await expect(editor.getByText('@')).toBeVisible();
|
||||||
),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks multiple big doc scroll to the top', async ({
|
test('it checks multiple big doc scroll to the top', async ({
|
||||||
@@ -844,13 +966,12 @@ test.describe('Doc Editor', () => {
|
|||||||
|
|
||||||
await expect(pdfBlock).toBeVisible();
|
await expect(pdfBlock).toBeVisible();
|
||||||
|
|
||||||
await page.getByText('Add PDF').click();
|
await page.getByText(/Add (PDF|file)/).click();
|
||||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
const downloadPromise = page.waitForEvent('download');
|
const downloadPromise = page.waitForEvent('download');
|
||||||
await page.getByText('Upload file').click();
|
await page.getByText(/Upload (PDF|file)/).click();
|
||||||
const fileChooser = await fileChooserPromise;
|
const fileChooser = await fileChooserPromise;
|
||||||
|
|
||||||
console.log(path.join(__dirname, 'assets/test-pdf.pdf'));
|
|
||||||
await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf'));
|
await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf'));
|
||||||
|
|
||||||
// Wait for the media-check to be processed
|
// Wait for the media-check to be processed
|
||||||
|
|||||||
@@ -1,19 +1,48 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import cs from 'convert-stream';
|
import cs from 'convert-stream';
|
||||||
import pdf from 'pdf-parse';
|
import { PDFParse } from 'pdf-parse';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TestLanguage,
|
TestLanguage,
|
||||||
createDoc,
|
createDoc,
|
||||||
randomName,
|
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
waitForLanguageSwitch,
|
waitForLanguageSwitch,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
import { createRootSubPage } from './utils-sub-pages';
|
||||||
|
|
||||||
|
const REGRESSION_FIXTURE_CONTENT = fs.readFileSync(
|
||||||
|
path.join(__dirname, 'assets/content.txt'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const REGRESSION_SNAPSHOT_NAME = 'doc-export-regression.pdf';
|
||||||
|
const REGRESSION_DOC_TITLE = 'doc-export-regression-reference';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright snapshots store the raw PDF bytes. However, each export embeds
|
||||||
|
* dynamic metadata (timestamps, font-subset identifiers, etc.) that would make
|
||||||
|
* the snapshot differ at every run. To ensure deterministic comparisons we
|
||||||
|
* strip/neutralize those fields before matching against the reference PDF.
|
||||||
|
*/
|
||||||
|
const sanitizePdfBuffer = (buffer: Buffer) => {
|
||||||
|
const pdfText = buffer.toString('latin1');
|
||||||
|
const neutralized = pdfText
|
||||||
|
// Remove per-export timestamps
|
||||||
|
.replace(/\/CreationDate\s*\(.*?\)/g, '/CreationDate ()')
|
||||||
|
.replace(/\/ModDate\s*\(.*?\)/g, '/ModDate ()')
|
||||||
|
// Remove file identifiers
|
||||||
|
.replace(/\/ID\s*\[<[^>]+>\s*<[^>]+>\]/g, '/ID [<0><0>]')
|
||||||
|
.replace(/D:\d{14}Z/g, 'D:00000000000000Z')
|
||||||
|
// Remove subset font prefixes generated by PDF renderer
|
||||||
|
.replace(/\b[A-Z]{6}\+(Inter18pt-[A-Za-z]+)\b/g, 'STATIC+$1')
|
||||||
|
.replace(/\b[A-Z]{6}\+(GeistMono-[A-Za-z]+)\b/g, 'STATIC+$1');
|
||||||
|
|
||||||
|
return Buffer.from(neutralized, 'latin1');
|
||||||
|
};
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
});
|
});
|
||||||
@@ -32,7 +61,7 @@ test.describe('Doc Export', () => {
|
|||||||
|
|
||||||
await expect(page.getByTestId('modal-export-title')).toBeVisible();
|
await expect(page.getByTestId('modal-export-title')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Download your document in a .docx or .pdf format.'),
|
page.getByText('Download your document in a .docx, .odt or .pdf format.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('combobox', { name: 'Template' }),
|
page.getByRole('combobox', { name: 'Template' }),
|
||||||
@@ -59,20 +88,16 @@ test.describe('Doc Export', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await verifyDocName(page, randomDoc);
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror.bn-editor');
|
const editor = await writeInEditor({ page, text: 'Hello' });
|
||||||
|
|
||||||
await editor.click();
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('Hello');
|
|
||||||
|
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
await openSuggestionMenu({ page });
|
||||||
await page.getByText('Page Break').click();
|
await page.getByText('Page Break').click();
|
||||||
|
|
||||||
await expect(editor.locator('.bn-page-break')).toBeVisible();
|
await expect(
|
||||||
|
editor.locator('div[data-content-type="pageBreak"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await page.keyboard.press('Enter');
|
await writeInEditor({ page, text: 'World' });
|
||||||
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('World');
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@@ -90,11 +115,16 @@ test.describe('Doc Export', () => {
|
|||||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||||
|
|
||||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||||
const pdfData = await pdf(pdfBuffer);
|
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||||
|
const pdfInfo = await pdfParse.getInfo();
|
||||||
|
const pdfText = await pdfParse.getText();
|
||||||
|
|
||||||
expect(pdfData.numpages).toBe(2);
|
expect(pdfInfo.total).toBe(2);
|
||||||
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
|
expect(pdfText.pages).toStrictEqual([
|
||||||
expect(pdfData.info.Title).toBe(randomDoc);
|
{ text: 'Hello', num: 1 },
|
||||||
|
{ text: 'World', num: 2 },
|
||||||
|
]);
|
||||||
|
expect(pdfInfo?.info.Title).toBe(randomDoc);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it exports the doc to docx', async ({ page, browserName }) => {
|
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||||
@@ -142,6 +172,51 @@ test.describe('Doc Export', () => {
|
|||||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
|
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it exports the doc to odt', async ({ page, browserName }) => {
|
||||||
|
const [randomDoc] = await createDoc(page, 'doc-editor-odt', browserName, 1);
|
||||||
|
|
||||||
|
await verifyDocName(page, randomDoc);
|
||||||
|
|
||||||
|
await page.locator('.ProseMirror.bn-editor').click();
|
||||||
|
await page.locator('.ProseMirror.bn-editor').fill('Hello World ODT');
|
||||||
|
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await page.getByText('Resizable image with caption').click();
|
||||||
|
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
|
await page.getByText('Upload image').click();
|
||||||
|
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
|
||||||
|
|
||||||
|
const image = page
|
||||||
|
.locator('.--docs--editor-container img.bn-visual-media')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
await expect(image).toBeVisible();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Export the document',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||||
|
await page.getByRole('option', { name: 'Odt' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('doc-export-download-button')).toBeVisible();
|
||||||
|
|
||||||
|
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||||
|
return download.suggestedFilename().includes(`${randomDoc}.odt`);
|
||||||
|
});
|
||||||
|
|
||||||
|
void page.getByTestId('doc-export-download-button').click();
|
||||||
|
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toBe(`${randomDoc}.odt`);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This test tell us that the export to pdf is working with images
|
* This test tell us that the export to pdf is working with images
|
||||||
* but it does not tell us if the images are being displayed correctly
|
* but it does not tell us if the images are being displayed correctly
|
||||||
@@ -223,10 +298,10 @@ test.describe('Doc Export', () => {
|
|||||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||||
|
|
||||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||||
const pdfExport = await pdf(pdfBuffer);
|
|
||||||
const pdfText = pdfExport.text;
|
|
||||||
|
|
||||||
expect(pdfText).toContain('Hello World');
|
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||||
|
const pdfText = await pdfParse.getText();
|
||||||
|
expect(pdfText.text).toContain('Hello World');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it exports the doc with quotes', async ({ page, browserName }) => {
|
test('it exports the doc with quotes', async ({ page, browserName }) => {
|
||||||
@@ -269,52 +344,9 @@ test.describe('Doc Export', () => {
|
|||||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||||
|
|
||||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||||
const pdfData = await pdf(pdfBuffer);
|
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||||
|
const pdfText = await pdfParse.getText();
|
||||||
expect(pdfData.text).toContain('Hello World'); // This is the pdf text
|
expect(pdfText.text).toContain('Hello World');
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We cannot assert the line break is visible in the pdf, but we can assert the
|
|
||||||
* line break is visible in the editor and that the pdf is generated.
|
|
||||||
*/
|
|
||||||
test('it exports the doc with divider', async ({ page, browserName }) => {
|
|
||||||
const [randomDoc] = await createDoc(page, 'export-divider', browserName, 1);
|
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
|
||||||
await editor.click();
|
|
||||||
await editor.fill('Hello World');
|
|
||||||
|
|
||||||
// Trigger slash menu to show menu
|
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
|
||||||
await page.getByText('Add a horizontal line').click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
editor.locator('.bn-block-content[data-content-type="divider"]'),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Export the document',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByTestId('doc-open-modal-download-button'),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
|
||||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
|
||||||
});
|
|
||||||
|
|
||||||
void page.getByTestId('doc-export-download-button').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('Hello World');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it exports the doc with multi columns', async ({
|
test('it exports the doc with multi columns', async ({
|
||||||
@@ -367,46 +399,33 @@ test.describe('Doc Export', () => {
|
|||||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||||
|
|
||||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||||
const pdfData = await pdf(pdfBuffer);
|
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||||
expect(pdfData.text).toContain('Column 1');
|
const pdfText = await pdfParse.getText();
|
||||||
expect(pdfData.text).toContain('Column 2');
|
expect(pdfText.text).toContain('Column 1');
|
||||||
expect(pdfData.text).toContain('Column 3');
|
expect(pdfText.text).toContain('Column 2');
|
||||||
|
expect(pdfText.text).toContain('Column 3');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it injects the correct language attribute into PDF export', async ({
|
test('it injects the correct language attribute into PDF export', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [randomDocFrench] = await createDoc(
|
||||||
|
page,
|
||||||
|
'doc-language-export-french',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
await waitForLanguageSwitch(page, TestLanguage.French);
|
await waitForLanguageSwitch(page, TestLanguage.French);
|
||||||
|
|
||||||
// Wait for the page to be ready after language switch
|
// Wait for the page to be ready after language switch
|
||||||
await page.waitForLoadState('domcontentloaded');
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
|
||||||
const header = page.locator('header').first();
|
await writeInEditor({
|
||||||
await header.locator('h1').getByText('Docs').click();
|
page,
|
||||||
|
text: 'Contenu de test pour export en français',
|
||||||
const randomDocFrench = randomName(
|
});
|
||||||
'doc-language-export-french',
|
|
||||||
browserName,
|
|
||||||
1,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Nouveau doc',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const input = page.getByRole('textbox', { name: 'Titre du document' });
|
|
||||||
await expect(input).toBeVisible();
|
|
||||||
await expect(input).toHaveText('', { timeout: 10000 });
|
|
||||||
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
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@@ -472,7 +491,7 @@ test.describe('Doc Export', () => {
|
|||||||
|
|
||||||
// Search the interlinking link in the editor (not in the document tree)
|
// Search the interlinking link in the editor (not in the document tree)
|
||||||
const editor = page.locator('.ProseMirror.bn-editor');
|
const editor = page.locator('.ProseMirror.bn-editor');
|
||||||
const interlink = editor.getByRole('link', {
|
const interlink = editor.getByRole('button', {
|
||||||
name: randomDoc,
|
name: randomDoc,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -494,8 +513,186 @@ test.describe('Doc Export', () => {
|
|||||||
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
|
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
|
||||||
|
|
||||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||||
const pdfData = await pdf(pdfBuffer);
|
const pdfParse = new PDFParse({ data: pdfBuffer });
|
||||||
|
const pdfText = await pdfParse.getText();
|
||||||
|
expect(pdfText.text).toContain(randomDoc);
|
||||||
|
});
|
||||||
|
|
||||||
expect(pdfData.text).toContain(randomDoc);
|
test('it exports the doc with interlinking to odt', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [randomDoc] = await createDoc(
|
||||||
|
page,
|
||||||
|
'export-interlinking-odt',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await verifyDocName(page, randomDoc);
|
||||||
|
|
||||||
|
const { name: docChild } = await createRootSubPage(
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
'export-interlink-child-odt',
|
||||||
|
);
|
||||||
|
|
||||||
|
await verifyDocName(page, docChild);
|
||||||
|
|
||||||
|
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('export-interlink');
|
||||||
|
|
||||||
|
await expect(searchContainer).toBeVisible();
|
||||||
|
await expect(searchContainer.getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
|
// We are in docChild, we want to create a link to randomDoc (parent)
|
||||||
|
await searchContainer.getByText(randomDoc).click();
|
||||||
|
|
||||||
|
// Search the interlinking link in the editor (not in the document tree)
|
||||||
|
const editor = page.locator('.ProseMirror.bn-editor');
|
||||||
|
const interlink = editor.getByRole('button', {
|
||||||
|
name: randomDoc,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(interlink).toBeVisible();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Export the document',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page.getByRole('combobox', { name: 'Format' }).click();
|
||||||
|
await page.getByRole('option', { name: 'Odt' }).click();
|
||||||
|
|
||||||
|
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||||
|
return download.suggestedFilename().includes(`${docChild}.odt`);
|
||||||
|
});
|
||||||
|
|
||||||
|
void page.getByTestId('doc-export-download-button').click();
|
||||||
|
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for the full PDF export pipeline.
|
||||||
|
*
|
||||||
|
* Usage reminder:
|
||||||
|
* 1. `npx playwright test __tests__/app-impress/doc-export.spec.ts --update-snapshots -g "full document" --project=chromium`
|
||||||
|
* -> refresh the reference PDF whenever we intentionally change the export output.
|
||||||
|
* 2. `npx playwright test __tests__/app-impress/doc-export.spec.ts -g "full document" --project=chromium`
|
||||||
|
* -> CI (and local runs without --update-snapshots) will compare the PDF to the reference
|
||||||
|
* and fail on any byte-level difference once the dynamic metadata has been sanitized.
|
||||||
|
*/
|
||||||
|
test('it keeps the full document PDF export identical to the reference snapshot', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}, testInfo) => {
|
||||||
|
// PDF generation for a large, image-heavy document can be slow in CI.
|
||||||
|
// Give this regression test a higher timeout budget than the default.
|
||||||
|
testInfo.setTimeout(120000);
|
||||||
|
const snapshotPath = testInfo.snapshotPath(REGRESSION_SNAPSHOT_NAME);
|
||||||
|
|
||||||
|
test.skip(
|
||||||
|
!fs.existsSync(snapshotPath) &&
|
||||||
|
testInfo.config.updateSnapshots === 'none',
|
||||||
|
`Missing PDF snapshot at ${snapshotPath}. Run Playwright with --update-snapshots to record it.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We must use a deterministic title so that block content (and thus the
|
||||||
|
// exported PDF) stays identical between runs.
|
||||||
|
await createDoc(page, 'doc-export-regression', browserName, 1);
|
||||||
|
const titleInput = page.getByRole('textbox', { name: 'Document title' });
|
||||||
|
await expect(titleInput).toBeVisible();
|
||||||
|
await titleInput.fill(REGRESSION_DOC_TITLE);
|
||||||
|
await titleInput.blur();
|
||||||
|
await verifyDocName(page, REGRESSION_DOC_TITLE);
|
||||||
|
const regressionDoc = REGRESSION_DOC_TITLE;
|
||||||
|
|
||||||
|
const docId = page
|
||||||
|
.url()
|
||||||
|
.split('/docs/')[1]
|
||||||
|
?.split('/')
|
||||||
|
.filter(Boolean)
|
||||||
|
.shift();
|
||||||
|
|
||||||
|
expect(docId).toBeTruthy();
|
||||||
|
|
||||||
|
// Inject the pre-crafted blocknote document via the REST API to avoid
|
||||||
|
// rebuilding it through the UI (which would be slow and flaky).
|
||||||
|
const cookies = await page.context().cookies();
|
||||||
|
const csrfToken = cookies.find(
|
||||||
|
(cookie) => cookie.name === 'csrftoken',
|
||||||
|
)?.value;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (csrfToken) {
|
||||||
|
headers['X-CSRFToken'] = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResponse = await page.request.patch(
|
||||||
|
`http://localhost:8071/api/v1.0/documents/${docId}/`,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
data: {
|
||||||
|
content: REGRESSION_FIXTURE_CONTENT,
|
||||||
|
websocket: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResponse.ok()) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to seed document content. Status: ${updateResponse.status()}, body: ${await updateResponse.text()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
// After reloading, just ensure the editor container is present before exporting.
|
||||||
|
await expect(page.locator('.--docs--editor-container')).toBeVisible({
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Export the document',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('doc-open-modal-download-button'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('doc-export-download-button')).toBeEnabled({
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export to PDF and confirm the generated bytes match the reference file.
|
||||||
|
const downloadPromise = page.waitForEvent('download', {
|
||||||
|
timeout: 60000,
|
||||||
|
predicate: (download) =>
|
||||||
|
download.suggestedFilename().includes(`${regressionDoc}.pdf`),
|
||||||
|
});
|
||||||
|
|
||||||
|
void page.getByTestId('doc-export-download-button').click();
|
||||||
|
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toBe(`${regressionDoc}.pdf`);
|
||||||
|
|
||||||
|
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||||
|
const normalizedPdfBuffer = sanitizePdfBuffer(pdfBuffer);
|
||||||
|
|
||||||
|
expect(normalizedPdfBuffer).toMatchSnapshot(REGRESSION_SNAPSHOT_NAME);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, getGridRow } from './utils-common';
|
import { createDoc, getGridRow, verifyDocName } from './utils-common';
|
||||||
|
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
||||||
|
|
||||||
type SmallDoc = {
|
type SmallDoc = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -11,7 +12,7 @@ test.describe('Documents Grid mobile', () => {
|
|||||||
test.use({ viewport: { width: 500, height: 1200 } });
|
test.use({ viewport: { width: 500, height: 1200 } });
|
||||||
|
|
||||||
test('it checks the grid when mobile', async ({ page }) => {
|
test('it checks the grid when mobile', async ({ page }) => {
|
||||||
await page.route('**/documents/**', async (route) => {
|
await page.route(/.*\/documents\/.*/, async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
if (request.method().includes('GET') && request.url().includes('page=')) {
|
if (request.method().includes('GET') && request.url().includes('page=')) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
@@ -133,13 +134,15 @@ test.describe('Document grid item options', () => {
|
|||||||
test('it deletes the document', async ({ page, browserName }) => {
|
test('it deletes the document', async ({ page, browserName }) => {
|
||||||
const [docTitle] = await createDoc(page, `delete doc`, browserName);
|
const [docTitle] = await createDoc(page, `delete doc`, browserName);
|
||||||
|
|
||||||
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await expect(page.getByText(docTitle)).toBeVisible();
|
await expect(page.getByText(docTitle)).toBeVisible();
|
||||||
const row = await getGridRow(page, docTitle);
|
const row = await getGridRow(page, docTitle);
|
||||||
await row.getByText(`more_horiz`).click();
|
await row.getByText(`more_horiz`).click();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||||
@@ -161,7 +164,7 @@ test.describe('Document grid item options', () => {
|
|||||||
test("it checks if the delete option is disabled if we don't have the destroy capability", async ({
|
test("it checks if the delete option is disabled if we don't have the destroy capability", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.route('*/**/api/v1.0/documents/?page=1', async (route) => {
|
await page.route(/.*\/api\/v1.0\/documents\/\?page=1/, async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
json: {
|
json: {
|
||||||
results: [
|
results: [
|
||||||
@@ -192,90 +195,68 @@ test.describe('Document grid item options', () => {
|
|||||||
});
|
});
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
const button = page.getByTestId(
|
const button = page
|
||||||
`docs-grid-actions-button-mocked-document-id`,
|
.getByTestId(`docs-grid-actions-button-mocked-document-id`)
|
||||||
);
|
.first();
|
||||||
await expect(button).toBeVisible();
|
await expect(button).toBeVisible();
|
||||||
await button.click();
|
await button.click();
|
||||||
const removeButton = page.getByTestId(
|
const removeButton = page
|
||||||
`docs-grid-actions-remove-mocked-document-id`,
|
.getByTestId(`docs-grid-actions-remove-mocked-document-id`)
|
||||||
);
|
.first();
|
||||||
await expect(removeButton).toBeVisible();
|
await expect(removeButton).toBeVisible();
|
||||||
await removeButton.isDisabled();
|
await removeButton.isDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Documents filters', () => {
|
test.describe('Documents filters', () => {
|
||||||
test('it checks the prebuild left panel filters', async ({ page }) => {
|
test('it checks the left panel filters', async ({ page, browserName }) => {
|
||||||
void page.goto('/');
|
void page.goto('/');
|
||||||
|
|
||||||
|
// Create my doc
|
||||||
|
const [docName] = await createDoc(page, 'my-doc', browserName, 1);
|
||||||
|
await verifyDocName(page, docName);
|
||||||
|
|
||||||
|
// Another user create a doc and share it with me
|
||||||
|
const { cleanup, otherPage, otherBrowserName } =
|
||||||
|
await connectOtherUserToDoc({
|
||||||
|
browserName,
|
||||||
|
docUrl: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [docShareName] = await createDoc(
|
||||||
|
otherPage,
|
||||||
|
'my-share-doc',
|
||||||
|
otherBrowserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await verifyDocName(otherPage, docShareName);
|
||||||
|
|
||||||
|
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
|
await addNewMember(otherPage, 0, 'Editor', browserName);
|
||||||
|
|
||||||
|
// Let's check the filters
|
||||||
|
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||||
|
|
||||||
|
const row = await getGridRow(page, docName);
|
||||||
|
const rowShare = await getGridRow(page, docShareName);
|
||||||
|
|
||||||
// All Docs
|
// All Docs
|
||||||
const response = await page.waitForResponse(
|
await expect(row).toBeVisible();
|
||||||
(response) =>
|
await expect(rowShare).toBeVisible();
|
||||||
response.url().endsWith('documents/?page=1') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
const allCount = result.count as number;
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
|
|
||||||
const allDocs = page.getByLabel('All docs');
|
// My Docs
|
||||||
const myDocs = page.getByLabel('My docs');
|
await page.getByRole('link', { name: 'My docs' }).click();
|
||||||
const sharedWithMe = page.getByLabel('Shared with me');
|
await expect(row).toBeVisible();
|
||||||
|
await expect(rowShare).toBeHidden();
|
||||||
// Initial state
|
|
||||||
await expect(allDocs).toBeVisible();
|
|
||||||
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).not.toHaveAttribute('aria-current');
|
|
||||||
|
|
||||||
await expect(sharedWithMe).toBeVisible();
|
|
||||||
await expect(sharedWithMe).toHaveCSS(
|
|
||||||
'background-color',
|
|
||||||
'rgba(0, 0, 0, 0)',
|
|
||||||
);
|
|
||||||
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');
|
|
||||||
|
|
||||||
// My docs
|
|
||||||
await myDocs.click();
|
|
||||||
url = new URL(page.url());
|
|
||||||
target = url.searchParams.get('target');
|
|
||||||
expect(target).toBe('my_docs');
|
|
||||||
const responseMyDocs = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const resultMyDocs = await responseMyDocs.json();
|
|
||||||
const countMyDocs = resultMyDocs.count as number;
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
expect(countMyDocs).toBeLessThanOrEqual(allCount);
|
|
||||||
|
|
||||||
// Shared with me
|
// Shared with me
|
||||||
await sharedWithMe.click();
|
await page.getByRole('link', { name: 'Shared with me' }).click();
|
||||||
url = new URL(page.url());
|
await expect(row).toBeHidden();
|
||||||
target = url.searchParams.get('target');
|
await expect(rowShare).toBeVisible();
|
||||||
expect(target).toBe('shared_with_me');
|
|
||||||
const responseSharedWithMe = await page.waitForResponse(
|
await cleanup();
|
||||||
(response) =>
|
|
||||||
response.url().includes('documents/?page=1&is_creator_me=false') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const resultSharedWithMe = await responseSharedWithMe.json();
|
|
||||||
const countSharedWithMe = resultSharedWithMe.count as number;
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
expect(countSharedWithMe).toBeLessThanOrEqual(allCount);
|
|
||||||
expect(countSharedWithMe + countMyDocs).toEqual(allCount);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { mockedAccesses, mockedInvitations } from './utils-share';
|
import { mockedAccesses, mockedInvitations } from './utils-share';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
import { createRootSubPage, getTreeRow } from './utils-sub-pages';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -65,25 +65,61 @@ test.describe('Doc Header', () => {
|
|||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await createDoc(page, 'doc-update', browserName, 1);
|
await createDoc(page, 'doc-update-emoji', browserName, 1);
|
||||||
|
|
||||||
|
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
|
||||||
|
const optionMenu = page.getByLabel('Open the document options');
|
||||||
|
const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' });
|
||||||
|
const removeEmojiMenuItem = page.getByRole('menuitem', {
|
||||||
|
name: 'Remove emoji',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Top parent should not have emoji picker
|
||||||
|
await expect(emojiPicker).toBeHidden();
|
||||||
|
await optionMenu.click();
|
||||||
|
await expect(addEmojiMenuItem).toBeHidden();
|
||||||
|
await expect(removeEmojiMenuItem).toBeHidden();
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
const { name: docChild } = await createRootSubPage(
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
'doc-update-emoji-child',
|
||||||
|
);
|
||||||
|
|
||||||
|
await verifyDocName(page, docChild);
|
||||||
|
|
||||||
|
// Emoji picker should be hidden initially
|
||||||
|
await expect(emojiPicker).toBeHidden();
|
||||||
|
|
||||||
|
// Add emoji
|
||||||
|
await optionMenu.click();
|
||||||
|
await expect(removeEmojiMenuItem).toBeHidden();
|
||||||
|
await addEmojiMenuItem.click();
|
||||||
|
await expect(emojiPicker).toHaveText('📄');
|
||||||
|
|
||||||
|
// Change emoji
|
||||||
|
await emojiPicker.click({
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
await page.getByRole('button', { name: '😀' }).first().click();
|
||||||
|
await expect(emojiPicker).toHaveText('😀');
|
||||||
|
|
||||||
|
// Update title
|
||||||
const docTitle = page.getByRole('textbox', { name: 'Document title' });
|
const docTitle = page.getByRole('textbox', { name: 'Document title' });
|
||||||
await expect(docTitle).toBeVisible();
|
await docTitle.fill('Hello Emoji World');
|
||||||
await docTitle.fill('👍 Hello Emoji World');
|
|
||||||
await docTitle.blur();
|
await docTitle.blur();
|
||||||
await verifyDocName(page, '👍 Hello Emoji World');
|
await verifyDocName(page, 'Hello Emoji World');
|
||||||
|
|
||||||
// Check the tree
|
// Check the tree
|
||||||
const docTree = page.getByTestId('doc-tree');
|
const row = await getTreeRow(page, 'Hello Emoji World');
|
||||||
await expect(docTree.getByText('Hello Emoji World')).toBeVisible();
|
await expect(row.getByText('😀')).toBeVisible();
|
||||||
await expect(docTree.getByTestId('doc-emoji-icon')).toBeVisible();
|
|
||||||
await expect(docTree.getByTestId('doc-simple-icon')).toBeHidden();
|
|
||||||
|
|
||||||
await page.getByTestId('home-button').click();
|
// Remove emoji
|
||||||
|
await optionMenu.click();
|
||||||
// Check the documents grid
|
await expect(addEmojiMenuItem).toBeHidden();
|
||||||
const gridRow = await getGridRow(page, 'Hello Emoji World');
|
await removeEmojiMenuItem.click();
|
||||||
await expect(gridRow.getByTestId('doc-emoji-icon')).toBeVisible();
|
await expect(emojiPicker).toBeHidden();
|
||||||
await expect(gridRow.getByTestId('doc-simple-icon')).toBeHidden();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it deletes the doc', async ({ page, browserName }) => {
|
test('it deletes the doc', async ({ page, browserName }) => {
|
||||||
@@ -96,7 +132,7 @@ test.describe('Doc Header', () => {
|
|||||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByText(`This document and any sub-`)).toBeVisible();
|
await expect(page.getByText(`This document will be`)).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@@ -172,7 +208,7 @@ test.describe('Doc Header', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
invitationCard.getByText('test.test@invitation.test').first(),
|
invitationCard.getByText('test.test@invitation.test').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
const invitationRole = invitationCard.getByLabel('doc-role-dropdown');
|
const invitationRole = invitationCard.getByTestId('doc-role-dropdown');
|
||||||
await expect(invitationRole).toBeVisible();
|
await expect(invitationRole).toBeVisible();
|
||||||
|
|
||||||
await invitationRole.click();
|
await invitationRole.click();
|
||||||
@@ -181,7 +217,7 @@ test.describe('Doc Header', () => {
|
|||||||
await expect(invitationCard).toBeHidden();
|
await expect(invitationCard).toBeHidden();
|
||||||
|
|
||||||
const memberCard = shareModal.getByLabel('List members card');
|
const memberCard = shareModal.getByLabel('List members card');
|
||||||
const roles = memberCard.getByLabel('doc-role-dropdown');
|
const roles = memberCard.getByTestId('doc-role-dropdown');
|
||||||
await expect(memberCard).toBeVisible();
|
await expect(memberCard).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
memberCard.getByText('test.test@accesses.test').first(),
|
memberCard.getByText('test.test@accesses.test').first(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
randomName,
|
randomName,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
|
import { writeInEditor } from './utils-editor';
|
||||||
import { connectOtherUserToDoc, updateRoleUser } from './utils-share';
|
import { connectOtherUserToDoc, updateRoleUser } from './utils-share';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
import { createRootSubPage } from './utils-sub-pages';
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ test.describe('Document create member', () => {
|
|||||||
await expect(list.getByText(email)).toBeVisible();
|
await expect(list.getByText(email)).toBeVisible();
|
||||||
|
|
||||||
// Check roles are displayed
|
// Check roles are displayed
|
||||||
await list.getByLabel('doc-role-dropdown').click();
|
await list.getByTestId('doc-role-dropdown').click();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible();
|
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible();
|
||||||
@@ -83,7 +84,7 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByTestId('doc-share-invite-button').click();
|
||||||
|
|
||||||
// Check invitation added
|
// Check invitation added
|
||||||
await expect(
|
await expect(
|
||||||
@@ -127,14 +128,14 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
const container = page.getByTestId('doc-share-add-member-list');
|
const container = page.getByTestId('doc-share-add-member-list');
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await container.getByTestId('doc-role-dropdown').click();
|
||||||
await page.getByRole('menuitem', { name: 'Owner' }).click();
|
await page.getByRole('menuitem', { name: 'Owner' }).click();
|
||||||
|
|
||||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('/invitations/') && response.status() === 201,
|
response.url().includes('/invitations/') && response.status() === 201,
|
||||||
);
|
);
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByTestId('doc-share-invite-button').click();
|
||||||
|
|
||||||
// Check invitation sent
|
// Check invitation sent
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ test.describe('Document create member', () => {
|
|||||||
await page.getByTestId(`search-user-row-${email}`).click();
|
await page.getByTestId(`search-user-row-${email}`).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await container.getByTestId('doc-role-dropdown').click();
|
||||||
await page.getByRole('menuitem', { name: 'Owner' }).click();
|
await page.getByRole('menuitem', { name: 'Owner' }).click();
|
||||||
|
|
||||||
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
||||||
@@ -153,7 +154,7 @@ test.describe('Document create member', () => {
|
|||||||
response.url().includes('/invitations/') && response.status() === 400,
|
response.url().includes('/invitations/') && response.status() === 400,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByTestId('doc-share-invite-button').click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(`"${email}" is already invited to the document.`),
|
page.getByText(`"${email}" is already invited to the document.`),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -182,7 +183,7 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
const container = page.getByTestId('doc-share-add-member-list');
|
const container = page.getByTestId('doc-share-add-member-list');
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await container.getByTestId('doc-role-dropdown').click();
|
||||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||||
|
|
||||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||||
@@ -190,7 +191,7 @@ test.describe('Document create member', () => {
|
|||||||
response.url().includes('/invitations/') && response.status() === 201,
|
response.url().includes('/invitations/') && response.status() === 201,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByTestId('doc-share-invite-button').click();
|
||||||
|
|
||||||
// Check invitation sent
|
// Check invitation sent
|
||||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||||
@@ -202,15 +203,21 @@ test.describe('Document create member', () => {
|
|||||||
);
|
);
|
||||||
await expect(userInvitation).toBeVisible();
|
await expect(userInvitation).toBeVisible();
|
||||||
|
|
||||||
await userInvitation.getByLabel('doc-role-dropdown').click();
|
const responsePromisePatchInvitation = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/invitations/') &&
|
||||||
|
response.status() === 200 &&
|
||||||
|
response.request().method() === 'PATCH',
|
||||||
|
);
|
||||||
|
|
||||||
|
await userInvitation.getByTestId('doc-role-dropdown').click();
|
||||||
await page.getByRole('menuitem', { name: 'Reader' }).click();
|
await page.getByRole('menuitem', { name: 'Reader' }).click();
|
||||||
|
|
||||||
const moreActions = userInvitation.getByRole('button', {
|
const responsePatchInvitation = await responsePromisePatchInvitation;
|
||||||
name: 'Open invitation actions menu',
|
expect(responsePatchInvitation.ok()).toBeTruthy();
|
||||||
});
|
|
||||||
await moreActions.click();
|
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await userInvitation.getByTestId('doc-role-dropdown').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Remove access' }).click();
|
||||||
|
|
||||||
await expect(userInvitation).toBeHidden();
|
await expect(userInvitation).toBeHidden();
|
||||||
});
|
});
|
||||||
@@ -230,11 +237,7 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
await page
|
await writeInEditor({ page, text: 'Hello World' });
|
||||||
.locator('.ProseMirror')
|
|
||||||
.locator('.bn-block-outer')
|
|
||||||
.last()
|
|
||||||
.fill('Hello World');
|
|
||||||
|
|
||||||
const docUrl = page.url();
|
const docUrl = page.url();
|
||||||
|
|
||||||
@@ -265,7 +268,7 @@ test.describe('Document create member', () => {
|
|||||||
const container = page.getByTestId(
|
const container = page.getByTestId(
|
||||||
`doc-share-access-request-row-${emailRequest}`,
|
`doc-share-access-request-row-${emailRequest}`,
|
||||||
);
|
);
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await container.getByTestId('doc-role-dropdown').click();
|
||||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||||
await container.getByRole('button', { name: 'Approve' }).click();
|
await container.getByRole('button', { name: 'Approve' }).click();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
|
import { createDoc, verifyDocName } from './utils-common';
|
||||||
import { addNewMember } from './utils-share';
|
import { addNewMember } from './utils-share';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -8,8 +8,14 @@ test.beforeEach(async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Document list members', () => {
|
test.describe('Document list members', () => {
|
||||||
test('it checks a big list of members', async ({ page }) => {
|
test('it checks a big list of members', async ({ page, browserName }) => {
|
||||||
const docTitle = await goToGridDoc(page);
|
const [docTitle] = await createDoc(
|
||||||
|
page,
|
||||||
|
'members-big-members-list',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
// Get the current URL and extract the last part
|
// Get the current URL and extract the last part
|
||||||
@@ -25,7 +31,7 @@ test.describe('Document list members', () => {
|
|||||||
return cleanUrl.split('/').pop() || '';
|
return cleanUrl.split('/').pop() || '';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
await page.route('**/documents/**/accesses/', async (route) => {
|
await page.route(/.*\/documents\/.*\/accesses\//, async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
const url = new URL(request.url());
|
const url = new URL(request.url());
|
||||||
const pageId = url.searchParams.get('page') ?? '1';
|
const pageId = url.searchParams.get('page') ?? '1';
|
||||||
@@ -73,7 +79,7 @@ test.describe('Document list members', () => {
|
|||||||
await expect(loadMore).toBeHidden();
|
await expect(loadMore).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks a big list of invitations', async ({ page }) => {
|
test('it checks a big list of invitations', async ({ page, browserName }) => {
|
||||||
await page.route(
|
await page.route(
|
||||||
/.*\/documents\/.*\/invitations\/\?page=.*/,
|
/.*\/documents\/.*\/invitations\/\?page=.*/,
|
||||||
async (route) => {
|
async (route) => {
|
||||||
@@ -108,7 +114,12 @@ test.describe('Document list members', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const docTitle = await goToGridDoc(page);
|
const [docTitle] = await createDoc(
|
||||||
|
page,
|
||||||
|
'members-big-invitation-list',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
@@ -141,7 +152,7 @@ test.describe('Document list members', () => {
|
|||||||
const currentUser = list.getByTestId(
|
const currentUser = list.getByTestId(
|
||||||
`doc-share-member-row-user.test@${browserName}.test`,
|
`doc-share-member-row-user.test@${browserName}.test`,
|
||||||
);
|
);
|
||||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||||
await expect(currentUser).toBeVisible();
|
await expect(currentUser).toBeVisible();
|
||||||
await expect(currentUserRole).toBeVisible();
|
await expect(currentUserRole).toBeVisible();
|
||||||
await currentUserRole.click();
|
await currentUserRole.click();
|
||||||
@@ -149,13 +160,16 @@ 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.`,
|
`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 expect(soloOwner).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Administrator' }),
|
||||||
|
).toBeDisabled();
|
||||||
|
|
||||||
await list.click({
|
await list.click({
|
||||||
force: true, // Force click to close the dropdown
|
force: true, // Force click to close the dropdown
|
||||||
});
|
});
|
||||||
const newUserEmail = await addNewMember(page, 0, 'Owner');
|
const newUserEmail = await addNewMember(page, 0, 'Owner');
|
||||||
const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`);
|
const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`);
|
||||||
const newUserRoles = newUser.getByLabel('doc-role-dropdown');
|
const newUserRoles = newUser.getByTestId('doc-role-dropdown');
|
||||||
|
|
||||||
await expect(newUser).toBeVisible();
|
await expect(newUser).toBeVisible();
|
||||||
|
|
||||||
@@ -175,9 +189,17 @@ test.describe('Document list members', () => {
|
|||||||
await list.click();
|
await list.click();
|
||||||
await expect(currentUserRole).toBeVisible();
|
await expect(currentUserRole).toBeVisible();
|
||||||
|
|
||||||
|
await newUserRoles.click();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeDisabled();
|
||||||
|
await list.click({
|
||||||
|
force: true, // Force click to close the dropdown
|
||||||
|
});
|
||||||
|
|
||||||
await currentUserRole.click();
|
await currentUserRole.click();
|
||||||
await page.getByRole('menuitem', { name: 'Reader' }).click();
|
await page.getByRole('menuitem', { name: 'Reader' }).click();
|
||||||
await list.click();
|
await list.click({
|
||||||
|
force: true, // Force click to close the dropdown
|
||||||
|
});
|
||||||
await expect(currentUserRole).toBeHidden();
|
await expect(currentUserRole).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,9 +214,7 @@ test.describe('Document list members', () => {
|
|||||||
|
|
||||||
const emailMyself = `user.test@${browserName}.test`;
|
const emailMyself = `user.test@${browserName}.test`;
|
||||||
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
|
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
|
||||||
const mySelfRole = mySelf.getByRole('button', {
|
const mySelfRole = mySelf.getByTestId('doc-role-dropdown');
|
||||||
name: 'doc-role-dropdown',
|
|
||||||
});
|
|
||||||
|
|
||||||
const userOwnerEmail = await addNewMember(page, 0, 'Owner');
|
const userOwnerEmail = await addNewMember(page, 0, 'Owner');
|
||||||
const userOwner = list.getByTestId(
|
const userOwner = list.getByTestId(
|
||||||
@@ -209,9 +229,7 @@ test.describe('Document list members', () => {
|
|||||||
const userReader = list.getByTestId(
|
const userReader = list.getByTestId(
|
||||||
`doc-share-member-row-${userReaderEmail}`,
|
`doc-share-member-row-${userReaderEmail}`,
|
||||||
);
|
);
|
||||||
const userReaderRole = userReader.getByRole('button', {
|
const userReaderRole = userReader.getByTestId('doc-role-dropdown');
|
||||||
name: 'doc-role-dropdown',
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(mySelf).toBeVisible();
|
await expect(mySelf).toBeVisible();
|
||||||
await expect(userOwner).toBeVisible();
|
await expect(userOwner).toBeVisible();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
mockedDocument,
|
mockedDocument,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
|
import { writeInEditor } from './utils-editor';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
import { createRootSubPage } from './utils-sub-pages';
|
||||||
|
|
||||||
test.describe('Doc Routing', () => {
|
test.describe('Doc Routing', () => {
|
||||||
@@ -58,37 +59,39 @@ test.describe('Doc Routing', () => {
|
|||||||
|
|
||||||
await createRootSubPage(page, browserName, '401-doc-child');
|
await createRootSubPage(page, browserName, '401-doc-child');
|
||||||
|
|
||||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
await writeInEditor({ page, text: 'Hello World' });
|
||||||
|
|
||||||
// Wait for the doc link (via its dynamic title) to be visible
|
const responsePromise = page.route(
|
||||||
const docLink = page.getByRole('link', { name: docTitle });
|
/.*\/documents\/.*\/$|users\/me\/$/,
|
||||||
await expect(docLink).toBeVisible();
|
async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
|
||||||
// Intercept GET/PATCH requests to return 401
|
// When we quit a document, a PATCH request is sent to save the document.
|
||||||
await page.route(/.*\/documents\/.*\/$|users\/me\/$/, async (route) => {
|
// We intercept this request to simulate a 401 error from the backend.
|
||||||
const request = route.request();
|
// The GET request to users/me is also intercepted to simulate the user
|
||||||
if (
|
// being logged out when trying to fetch user info.
|
||||||
request.method().includes('PATCH') ||
|
// This way we can test the 401 error handling when saving the document
|
||||||
request.method().includes('GET')
|
if (
|
||||||
) {
|
(request.url().includes('/documents/') &&
|
||||||
await route.fulfill({
|
request.method().includes('PATCH')) ||
|
||||||
status: 401,
|
(request.url().includes('/users/me/') &&
|
||||||
json: { detail: 'Log in to access the document' },
|
request.method().includes('GET'))
|
||||||
});
|
) {
|
||||||
} else {
|
await route.fulfill({
|
||||||
await route.continue();
|
status: 401,
|
||||||
}
|
json: {
|
||||||
});
|
detail: 'Log in to access the document',
|
||||||
|
},
|
||||||
// Explicitly wait for a 401 response after clicking
|
});
|
||||||
const wait401 = page.waitForResponse(
|
} else {
|
||||||
(resp) =>
|
await route.continue();
|
||||||
resp.status() === 401 &&
|
}
|
||||||
/\/(documents\/[^/]+\/|users\/me\/)$/.test(resp.url()),
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await docLink.click();
|
await page.getByRole('link', { name: '401-doc-parent' }).click();
|
||||||
await wait401;
|
|
||||||
|
await responsePromise;
|
||||||
|
|
||||||
await expect(page.getByText('Log in to access the document.')).toBeVisible({
|
await expect(page.getByText('Log in to access the document.')).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
|
|||||||
155
src/frontend/apps/e2e/__tests__/app-impress/doc-trashbin.spec.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
clickInEditorMenu,
|
||||||
|
clickInGridMenu,
|
||||||
|
createDoc,
|
||||||
|
getGridRow,
|
||||||
|
verifyDocName,
|
||||||
|
} from './utils-common';
|
||||||
|
import { addNewMember } from './utils-share';
|
||||||
|
import {
|
||||||
|
addChild,
|
||||||
|
createRootSubPage,
|
||||||
|
navigateToPageFromTree,
|
||||||
|
} from './utils-sub-pages';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Doc Trashbin', () => {
|
||||||
|
test('it controls UI and interaction from the grid page', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [title1] = await createDoc(page, 'my-trash-doc-1', browserName, 1);
|
||||||
|
const [title2] = await createDoc(page, 'my-trash-doc-2', browserName, 1);
|
||||||
|
await verifyDocName(page, title2);
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await addNewMember(page, 0, 'Editor');
|
||||||
|
await page.getByRole('button', { name: 'close' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||||
|
|
||||||
|
const row1 = await getGridRow(page, title1);
|
||||||
|
await clickInGridMenu(page, row1, 'Delete');
|
||||||
|
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||||
|
await expect(row1.getByText(title1)).toBeHidden();
|
||||||
|
|
||||||
|
const row2 = await getGridRow(page, title2);
|
||||||
|
await clickInGridMenu(page, row2, 'Delete');
|
||||||
|
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||||
|
await expect(row2.getByText(title2)).toBeHidden();
|
||||||
|
|
||||||
|
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||||
|
|
||||||
|
const docsGrid = page.getByTestId('docs-grid');
|
||||||
|
await expect(docsGrid.getByText('Days remaining')).toBeVisible();
|
||||||
|
await expect(row1.getByText(title1)).toBeVisible();
|
||||||
|
await expect(row1.getByText('30 days')).toBeVisible();
|
||||||
|
await expect(row2.getByText(title2)).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
row2.getByRole('button', {
|
||||||
|
name: 'Open the sharing settings for the document',
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
row2.getByRole('button', {
|
||||||
|
name: 'Open the sharing settings for the document',
|
||||||
|
}),
|
||||||
|
).toBeDisabled();
|
||||||
|
|
||||||
|
await clickInGridMenu(page, row2, 'Restore');
|
||||||
|
|
||||||
|
await expect(row2.getByText(title2)).toBeHidden();
|
||||||
|
await page.getByRole('link', { name: 'All docs' }).click();
|
||||||
|
const row2Restored = await getGridRow(page, title2);
|
||||||
|
await expect(row2Restored.getByText(title2)).toBeVisible();
|
||||||
|
await row2Restored.getByRole('link', { name: /Open document/ }).click();
|
||||||
|
|
||||||
|
await verifyDocName(page, title2);
|
||||||
|
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||||
|
await expect(row2.getByText(title2)).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
row2.getByRole('button', {
|
||||||
|
name: 'Open the sharing settings for the document',
|
||||||
|
}),
|
||||||
|
).toBeEnabled();
|
||||||
|
|
||||||
|
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||||
|
await expect(row2.getByText(title2)).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it controls UI and interaction from the doc page', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [topParent] = await createDoc(
|
||||||
|
page,
|
||||||
|
'my-trash-editor-doc',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
await verifyDocName(page, topParent);
|
||||||
|
const { name: subDocName } = await createRootSubPage(
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
'my-trash-editor-subdoc',
|
||||||
|
);
|
||||||
|
|
||||||
|
const subsubDocName = await addChild({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
docParent: subDocName,
|
||||||
|
docName: 'my-trash-editor-subsubdoc',
|
||||||
|
});
|
||||||
|
await verifyDocName(page, subsubDocName);
|
||||||
|
|
||||||
|
await navigateToPageFromTree({ page, title: subDocName });
|
||||||
|
await verifyDocName(page, subDocName);
|
||||||
|
|
||||||
|
await clickInEditorMenu(page, 'Delete sub-document');
|
||||||
|
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||||
|
await verifyDocName(page, topParent);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||||
|
const row = await getGridRow(page, subDocName);
|
||||||
|
await row.getByText(subDocName).click();
|
||||||
|
await verifyDocName(page, subDocName);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator('.--docs--editor-container.--docs--doc-deleted'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
|
||||||
|
await expect(page.locator('.bn-editor')).toHaveAttribute(
|
||||||
|
'contenteditable',
|
||||||
|
'false',
|
||||||
|
);
|
||||||
|
const docTree = page.getByTestId('doc-tree');
|
||||||
|
await expect(docTree.getByText(topParent)).toBeHidden();
|
||||||
|
await expect(
|
||||||
|
docTree.getByText(subDocName, {
|
||||||
|
exact: true,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(docTree.getByText(subsubDocName)).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
docTree
|
||||||
|
.locator(".--docs-sub-page-item[aria-disabled='true']")
|
||||||
|
.getByText(subsubDocName),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Restore' }).click();
|
||||||
|
await expect(page.getByLabel('Alert deleted document')).toBeHidden();
|
||||||
|
await expect(page.locator('.bn-editor')).toHaveAttribute(
|
||||||
|
'contenteditable',
|
||||||
|
'true',
|
||||||
|
);
|
||||||
|
await expect(page.getByRole('button', { name: 'Share' })).toBeEnabled();
|
||||||
|
await expect(docTree.getByText(topParent)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { addNewMember } from './utils-share';
|
import { addNewMember } from './utils-share';
|
||||||
import { clickOnAddRootSubPage, createRootSubPage } from './utils-sub-pages';
|
import {
|
||||||
|
clickOnAddRootSubPage,
|
||||||
|
createRootSubPage,
|
||||||
|
getTreeRow,
|
||||||
|
} from './utils-sub-pages';
|
||||||
|
|
||||||
test.describe('Doc Tree', () => {
|
test.describe('Doc Tree', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -222,7 +226,7 @@ test.describe('Doc Tree', () => {
|
|||||||
const currentUser = list.getByTestId(
|
const currentUser = list.getByTestId(
|
||||||
`doc-share-member-row-user.test@${browserName}.test`,
|
`doc-share-member-row-user.test@${browserName}.test`,
|
||||||
);
|
);
|
||||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||||
await currentUserRole.click();
|
await currentUserRole.click();
|
||||||
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
await page.getByRole('menuitem', { name: 'Administrator' }).click();
|
||||||
await list.click();
|
await list.click();
|
||||||
@@ -298,6 +302,58 @@ test.describe('Doc Tree', () => {
|
|||||||
// Now test keyboard navigation on sub-document
|
// Now test keyboard navigation on sub-document
|
||||||
await expect(docTree.getByText(docChild)).toBeVisible();
|
await expect(docTree.getByText(docChild)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it updates the child icon from the tree', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [docParent] = await createDoc(
|
||||||
|
page,
|
||||||
|
'doc-child-emoji',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
await verifyDocName(page, docParent);
|
||||||
|
|
||||||
|
const { name: docChild } = await createRootSubPage(
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
'doc-child-emoji-child',
|
||||||
|
);
|
||||||
|
|
||||||
|
const row = await getTreeRow(page, docChild);
|
||||||
|
|
||||||
|
// Check Remove emoji is not present initially
|
||||||
|
await row.hover();
|
||||||
|
const menu = row.getByText(`more_horiz`);
|
||||||
|
await menu.click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: 'Remove emoji' }),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
// Close the menu
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
// Update the emoji from the tree
|
||||||
|
await row.locator('.--docs--doc-icon').click();
|
||||||
|
await page.getByRole('button', { name: '😀' }).first().click();
|
||||||
|
|
||||||
|
// Verify the emoji is updated in the tree and in the document title
|
||||||
|
await expect(row.getByText('😀')).toBeVisible();
|
||||||
|
|
||||||
|
const titleEmojiPicker = page
|
||||||
|
.locator('.--docs--doc-title')
|
||||||
|
.getByRole('button');
|
||||||
|
await expect(titleEmojiPicker).toHaveText('😀');
|
||||||
|
|
||||||
|
// Now remove the emoji using the new action
|
||||||
|
await row.hover();
|
||||||
|
await menu.click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
|
||||||
|
|
||||||
|
await expect(row.getByText('😀')).toBeHidden();
|
||||||
|
await expect(titleEmojiPicker).toBeHidden();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Tree: Inheritance', () => {
|
test.describe('Doc Tree: Inheritance', () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
keyCloakSignIn,
|
keyCloakSignIn,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
|
import { getEditor, writeInEditor } from './utils-editor';
|
||||||
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
import { addNewMember, connectOtherUserToDoc } from './utils-share';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
import { createRootSubPage } from './utils-sub-pages';
|
||||||
|
|
||||||
@@ -151,18 +152,15 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
await page
|
await writeInEditor({ page, text: 'Hello World' });
|
||||||
.locator('.ProseMirror')
|
|
||||||
.locator('.bn-block-outer')
|
|
||||||
.last()
|
|
||||||
.fill('Hello World');
|
|
||||||
|
|
||||||
const docUrl = page.url();
|
const docUrl = page.url();
|
||||||
|
|
||||||
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
|
const { otherBrowserName, otherPage, cleanup } =
|
||||||
browserName,
|
await connectOtherUserToDoc({
|
||||||
docUrl,
|
browserName,
|
||||||
});
|
docUrl,
|
||||||
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
otherPage.getByText('Insufficient access rights to view the document.'),
|
otherPage.getByText('Insufficient access rights to view the document.'),
|
||||||
@@ -175,20 +173,23 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
await addNewMember(page, 0, 'Reader', otherBrowserName);
|
await addNewMember(page, 0, 'Reader', otherBrowserName);
|
||||||
|
|
||||||
await otherPage.reload();
|
await otherPage.reload();
|
||||||
await expect(otherPage.getByText('Hello World')).toBeVisible();
|
await expect(otherPage.getByText('Hello World')).toBeVisible({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Visibility: Public', () => {
|
test.describe('Doc Visibility: Public', () => {
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
});
|
||||||
|
|
||||||
test('It checks a public doc in read only mode', async ({
|
test('It checks a public doc in read only mode', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto('/');
|
|
||||||
await keyCloakSignIn(page, browserName);
|
|
||||||
|
|
||||||
const [docTitle] = await createDoc(
|
const [docTitle] = await createDoc(
|
||||||
page,
|
page,
|
||||||
'Public read only',
|
'Public read only',
|
||||||
@@ -198,6 +199,8 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
|
await writeInEditor({ page, text: 'Hello Public Viewonly' });
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByTestId('doc-visibility');
|
const selectVisibility = page.getByTestId('doc-visibility');
|
||||||
await selectVisibility.click();
|
await selectVisibility.click();
|
||||||
@@ -239,49 +242,67 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
await expect(page.getByTestId('search-docs-button')).toBeVisible();
|
await expect(page.getByTestId('search-docs-button')).toBeVisible();
|
||||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const docUrl = page.url();
|
||||||
|
|
||||||
await page
|
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||||
.getByRole('button', {
|
browserName,
|
||||||
name: 'Logout',
|
docUrl,
|
||||||
})
|
withoutSignIn: true,
|
||||||
.click();
|
});
|
||||||
|
|
||||||
await expectLoginPage(page);
|
await expect(otherPage.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
|
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
|
||||||
await page.goto(urlDoc);
|
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
|
||||||
|
await expect(
|
||||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
otherPage.getByRole('button', { name: 'Share' }),
|
||||||
await expect(page.getByTestId('search-docs-button')).toBeHidden();
|
).toBeVisible();
|
||||||
await expect(page.getByTestId('new-doc-button')).toBeHidden();
|
const card = otherPage.getByLabel('It is the card information');
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
|
||||||
const card = page.getByLabel('It is the card information');
|
|
||||||
await expect(card).toBeVisible();
|
await expect(card).toBeVisible();
|
||||||
await expect(card.getByText('Reader')).toBeVisible();
|
await expect(card.getByText('Reader')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(
|
otherPage.locator('.--docs--editor-container.--docs--doc-readonly'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const otherEditor = await getEditor({ page: otherPage });
|
||||||
|
await expect(otherEditor).toHaveAttribute('contenteditable', 'false');
|
||||||
|
await expect(otherEditor.getByText('Hello Public Viewonly')).toBeVisible();
|
||||||
|
|
||||||
|
// Cursor and selection of the anonymous user are not visible
|
||||||
|
await otherEditor.getByText('Hello Public').selectText();
|
||||||
|
await expect(
|
||||||
|
page.locator('.collaboration-cursor-custom__base'),
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(page.locator('.ProseMirror-yjs-selection')).toBeHidden();
|
||||||
|
|
||||||
|
// Can still see changes made by others
|
||||||
|
await writeInEditor({ page, text: 'Can you see it ?' });
|
||||||
|
await expect(otherEditor.getByText('Can you see it ?')).toBeVisible();
|
||||||
|
|
||||||
|
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await expect(
|
||||||
|
otherPage.getByText(
|
||||||
'You can view this document but need additional access to see its members or modify settings.',
|
'You can view this document but need additional access to see its members or modify settings.',
|
||||||
),
|
),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Request access' }),
|
otherPage.getByRole('button', { name: 'Request access' }),
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('It checks a public doc in editable mode', async ({
|
test('It checks a public doc in editable mode', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto('/');
|
|
||||||
await keyCloakSignIn(page, browserName);
|
|
||||||
|
|
||||||
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
|
await writeInEditor({ page, text: 'Hello Public Editable' });
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByTestId('doc-visibility');
|
const selectVisibility = page.getByTestId('doc-visibility');
|
||||||
await selectVisibility.click();
|
await selectVisibility.click();
|
||||||
@@ -315,20 +336,47 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
cardContainer.getByText('Public document', { exact: true }),
|
cardContainer.getByText('Public document', { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const docUrl = page.url();
|
||||||
|
|
||||||
await page
|
const { otherPage, cleanup } = await connectOtherUserToDoc({
|
||||||
.getByRole('button', {
|
browserName,
|
||||||
name: 'Logout',
|
docUrl,
|
||||||
})
|
withoutSignIn: true,
|
||||||
.click();
|
docTitle,
|
||||||
|
});
|
||||||
|
|
||||||
await expectLoginPage(page);
|
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
|
||||||
|
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
|
||||||
|
|
||||||
await page.goto(urlDoc);
|
const otherEditor = await getEditor({ page: otherPage });
|
||||||
|
await expect(otherEditor).toHaveAttribute('contenteditable', 'true');
|
||||||
|
await expect(otherEditor.getByText('Hello Public Editable')).toBeVisible();
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
// We can see the collaboration cursor of the anonymous user
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
await otherEditor.getByText('Hello Public').selectText();
|
||||||
|
await expect(
|
||||||
|
page.locator('.collaboration-cursor-custom__base').getByText('Anonymous'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
otherPage.getByRole('button', { name: 'Share' }),
|
||||||
|
).toBeVisible();
|
||||||
|
const card = otherPage.getByLabel('It is the card information');
|
||||||
|
await expect(card).toBeVisible();
|
||||||
|
await expect(card.getByText('Editor')).toBeVisible();
|
||||||
|
|
||||||
|
await otherPage.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await expect(
|
||||||
|
otherPage.getByText(
|
||||||
|
'You can view this document but need additional access to see its members or modify settings.',
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
otherPage.getByRole('button', { name: 'Request access' }),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
await cleanup();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ test.describe('Header', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
await overrideConfig(page, {
|
await overrideConfig(page, {
|
||||||
FRONTEND_THEME: 'dsfr',
|
FRONTEND_THEME: 'dsfr',
|
||||||
|
theme_customization: {
|
||||||
|
header: {
|
||||||
|
logo: {
|
||||||
|
src: '/assets/logo-gouv.svg',
|
||||||
|
width: '220px',
|
||||||
|
alt: 'Gouvernement Logo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
@@ -98,6 +107,15 @@ test.describe('Header mobile', () => {
|
|||||||
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
|
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
|
||||||
await overrideConfig(page, {
|
await overrideConfig(page, {
|
||||||
FRONTEND_THEME: 'dsfr',
|
FRONTEND_THEME: 'dsfr',
|
||||||
|
theme_customization: {
|
||||||
|
header: {
|
||||||
|
logo: {
|
||||||
|
src: '/assets/logo-gouv.svg',
|
||||||
|
width: '220px',
|
||||||
|
alt: 'Gouvernement Logo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -131,3 +149,31 @@ test.describe('Header: Log out', () => {
|
|||||||
await expectLoginPage(page);
|
await expectLoginPage(page);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Header: Override configuration', () => {
|
||||||
|
test('checks the header is correctly overrided', async ({ page }) => {
|
||||||
|
await overrideConfig(page, {
|
||||||
|
FRONTEND_THEME: 'dsfr',
|
||||||
|
theme_customization: {
|
||||||
|
header: {
|
||||||
|
icon: {
|
||||||
|
src: '/assets/logo-gouv.svg',
|
||||||
|
width: '220px',
|
||||||
|
height: 'auto',
|
||||||
|
alt: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
const header = page.locator('header').first();
|
||||||
|
|
||||||
|
const logoImage = header.getByTestId('header-icon-docs');
|
||||||
|
await expect(logoImage).toBeVisible();
|
||||||
|
|
||||||
|
await expect(logoImage).not.toHaveAttribute('src', '/assets/icon-docs.svg');
|
||||||
|
await expect(logoImage).toHaveAttribute('src', '/assets/logo-gouv.svg');
|
||||||
|
await expect(logoImage).toHaveAttribute('alt', '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,6 +27,33 @@ test.describe('Home page', () => {
|
|||||||
// Check the titles
|
// Check the titles
|
||||||
const h2 = page.locator('h2');
|
const h2 = page.locator('h2');
|
||||||
await expect(h2.getByText('Govs ❤️ Open Source.')).toBeVisible();
|
await expect(h2.getByText('Govs ❤️ Open Source.')).toBeVisible();
|
||||||
|
await expect(page.getByText('Docs is built on top of')).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('link', {
|
||||||
|
name: 'Django Rest Framework',
|
||||||
|
}),
|
||||||
|
).toHaveAttribute('href', 'https://www.django-rest-framework.org/');
|
||||||
|
await expect(page.getByText('You can easily self-host Docs')).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByRole('link', {
|
||||||
|
name: 'licence',
|
||||||
|
})
|
||||||
|
.first(),
|
||||||
|
).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'https://github.com/suitenumerique/docs/blob/main/LICENSE',
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
page.getByText('Docs is the result of a joint effort lead by the French'),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByRole('link', {
|
||||||
|
name: 'Zendis',
|
||||||
|
})
|
||||||
|
.first(),
|
||||||
|
).toHaveAttribute('href', 'https://zendis.de/');
|
||||||
await expect(
|
await expect(
|
||||||
h2.getByText('Collaborative writing, Simplified.'),
|
h2.getByText('Collaborative writing, Simplified.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
|
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
|
||||||
|
import { openSuggestionMenu } from './utils-editor';
|
||||||
|
|
||||||
test.describe('Language', () => {
|
test.describe('Language', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -51,6 +52,7 @@ test.describe('Language', () => {
|
|||||||
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
|
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
|
||||||
await expect(languagePicker).toContainText('English');
|
await expect(languagePicker).toContainText('English');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can switch language using only keyboard', async ({ page }) => {
|
test('can switch language using only keyboard', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await waitForLanguageSwitch(page, TestLanguage.English);
|
await waitForLanguageSwitch(page, TestLanguage.English);
|
||||||
@@ -106,18 +108,18 @@ test.describe('Language', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
await createDoc(page, 'doc-toolbar', browserName, 1);
|
await createDoc(page, 'doc-toolbar', browserName, 1);
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = await openSuggestionMenu({ page });
|
||||||
|
|
||||||
// Trigger slash menu to show english menu
|
|
||||||
await editor.click();
|
|
||||||
await editor.fill('/');
|
|
||||||
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
|
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await editor.click(); // close the menu
|
||||||
|
|
||||||
|
await expect(page.getByText('Headings', { exact: true })).toBeHidden();
|
||||||
|
|
||||||
// Change language to French
|
// Change language to French
|
||||||
await waitForLanguageSwitch(page, TestLanguage.French);
|
await waitForLanguageSwitch(page, TestLanguage.French);
|
||||||
|
|
||||||
// Trigger slash menu to show french menu
|
// Trigger slash menu to show french menu
|
||||||
await editor.locator('.bn-block-outer').last().fill('/');
|
await openSuggestionMenu({ page });
|
||||||
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
|
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
|
||||||
|
|
||||||
test.describe('Left panel desktop', () => {
|
test.describe('Left panel desktop', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -8,8 +10,59 @@ test.describe('Left panel desktop', () => {
|
|||||||
test('checks all the elements are visible', async ({ page }) => {
|
test('checks all the elements are visible', async ({ page }) => {
|
||||||
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
|
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
|
||||||
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
|
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
|
||||||
await expect(page.getByTestId('home-button')).toBeVisible();
|
await expect(page.getByTestId('home-button')).toBeHidden();
|
||||||
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
await expect(page.getByTestId('new-doc-button')).toBeVisible();
|
||||||
|
|
||||||
|
await goToGridDoc(page);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('home-button')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checks resize handle is present and functional on document page', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
// On home page, resize handle should NOT be present
|
||||||
|
let resizeHandle = page.locator('[data-panel-resize-handle-id]');
|
||||||
|
await expect(resizeHandle).toBeHidden();
|
||||||
|
|
||||||
|
// Create and navigate to a document
|
||||||
|
await createDoc(page, 'doc-resize-test', browserName, 1);
|
||||||
|
|
||||||
|
// Now resize handle should be visible on document page
|
||||||
|
resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
|
||||||
|
await expect(resizeHandle).toBeVisible();
|
||||||
|
|
||||||
|
const leftPanel = page.getByTestId('left-panel-desktop');
|
||||||
|
await expect(leftPanel).toBeVisible();
|
||||||
|
|
||||||
|
// Get initial panel width
|
||||||
|
const initialBox = await leftPanel.boundingBox();
|
||||||
|
expect(initialBox).not.toBeNull();
|
||||||
|
|
||||||
|
// Get handle position
|
||||||
|
const handleBox = await resizeHandle.boundingBox();
|
||||||
|
expect(handleBox).not.toBeNull();
|
||||||
|
|
||||||
|
// Test resize by dragging the handle
|
||||||
|
await page.mouse.move(
|
||||||
|
handleBox!.x + handleBox!.width / 2,
|
||||||
|
handleBox!.y + handleBox!.height / 2,
|
||||||
|
);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(
|
||||||
|
handleBox!.x + 100,
|
||||||
|
handleBox!.y + handleBox!.height / 2,
|
||||||
|
);
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
// Wait for resize to complete
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
|
// Verify the panel has been resized
|
||||||
|
const newBox = await leftPanel.boundingBox();
|
||||||
|
expect(newBox).not.toBeNull();
|
||||||
|
expect(newBox!.width).toBeGreaterThan(initialBox!.width);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,6 +92,9 @@ test.describe('Left panel mobile', () => {
|
|||||||
await expect(languageButton).not.toBeInViewport();
|
await expect(languageButton).not.toBeInViewport();
|
||||||
await expect(logoutButton).not.toBeInViewport();
|
await expect(logoutButton).not.toBeInViewport();
|
||||||
|
|
||||||
|
const title = await goToGridDoc(page);
|
||||||
|
await verifyDocName(page, title);
|
||||||
|
|
||||||
await header.getByLabel('Open the header menu').click();
|
await header.getByLabel('Open the header menu').click();
|
||||||
|
|
||||||
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();
|
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();
|
||||||
@@ -47,4 +103,12 @@ test.describe('Left panel mobile', () => {
|
|||||||
await expect(languageButton).toBeInViewport();
|
await expect(languageButton).toBeInViewport();
|
||||||
await expect(logoutButton).toBeInViewport();
|
await expect(logoutButton).toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('checks resize handle is not present on mobile', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Verify the resize handle is NOT present on mobile
|
||||||
|
const resizeHandle = page.locator('[data-panel-resize-handle-id]');
|
||||||
|
await expect(resizeHandle).toBeHidden();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Page, expect } from '@playwright/test';
|
import { Locator, Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||||
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
|
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
|
||||||
@@ -23,6 +23,7 @@ export const CONFIG = {
|
|||||||
LANGUAGE_CODE: 'en-us',
|
LANGUAGE_CODE: 'en-us',
|
||||||
POSTHOG_KEY: {},
|
POSTHOG_KEY: {},
|
||||||
SENTRY_DSN: null,
|
SENTRY_DSN: null,
|
||||||
|
TRASHBIN_CUTOFF_DAYS: 30,
|
||||||
theme_customization: {},
|
theme_customization: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export const overrideConfig = async (
|
|||||||
page: Page,
|
page: Page,
|
||||||
newConfig: { [_K in keyof typeof CONFIG]?: unknown },
|
newConfig: { [_K in keyof typeof CONFIG]?: unknown },
|
||||||
) =>
|
) =>
|
||||||
await page.route('**/api/v1.0/config/', async (route) => {
|
await page.route(/.*\/api\/v1.0\/config\/.*/, async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
if (request.method().includes('GET')) {
|
if (request.method().includes('GET')) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
@@ -47,7 +48,7 @@ export const overrideConfig = async (
|
|||||||
export const keyCloakSignIn = async (
|
export const keyCloakSignIn = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
browserName: string,
|
browserName: string,
|
||||||
fromHome: boolean = true,
|
fromHome = true,
|
||||||
) => {
|
) => {
|
||||||
if (fromHome) {
|
if (fromHome) {
|
||||||
await page.getByRole('button', { name: 'Start Writing' }).first().click();
|
await page.getByRole('button', { name: 'Start Writing' }).first().click();
|
||||||
@@ -69,6 +70,14 @@ export const keyCloakSignIn = async (
|
|||||||
await page.click('button[type="submit"]', { force: true });
|
await page.click('button[type="submit"]', { force: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getOtherBrowserName = (browserName: BrowserName) => {
|
||||||
|
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
|
||||||
|
if (!otherBrowserName) {
|
||||||
|
throw new Error('No alternative browser found');
|
||||||
|
}
|
||||||
|
return otherBrowserName;
|
||||||
|
};
|
||||||
|
|
||||||
export const randomName = (name: string, browserName: string, length: number) =>
|
export const randomName = (name: string, browserName: string, length: number) =>
|
||||||
Array.from({ length }, (_el, index) => {
|
Array.from({ length }, (_el, index) => {
|
||||||
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;
|
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;
|
||||||
@@ -78,8 +87,8 @@ export const createDoc = async (
|
|||||||
page: Page,
|
page: Page,
|
||||||
docName: string,
|
docName: string,
|
||||||
browserName: string,
|
browserName: string,
|
||||||
length: number = 1,
|
length = 1,
|
||||||
isMobile: boolean = false,
|
isMobile = false,
|
||||||
) => {
|
) => {
|
||||||
const randomDocs = randomName(docName, browserName, length);
|
const randomDocs = randomName(docName, browserName, length);
|
||||||
|
|
||||||
@@ -124,7 +133,9 @@ export const verifyDocName = async (page: Page, docName: string) => {
|
|||||||
try {
|
try {
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('textbox', { name: 'Document title' }),
|
page.getByRole('textbox', { name: 'Document title' }),
|
||||||
).toContainText(docName);
|
).toContainText(docName, {
|
||||||
|
timeout: 1000,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
|
await expect(page.getByRole('heading', { name: docName })).toBeVisible();
|
||||||
}
|
}
|
||||||
@@ -203,7 +214,7 @@ export const waitForResponseCreateDoc = (page: Page) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const mockedDocument = async (page: Page, data: object) => {
|
export const mockedDocument = async (page: Page, data: object) => {
|
||||||
await page.route('**/documents/**/', async (route) => {
|
await page.route(/\**\/documents\/\**/, async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
if (
|
if (
|
||||||
request.method().includes('GET') &&
|
request.method().includes('GET') &&
|
||||||
@@ -255,7 +266,7 @@ export const mockedDocument = async (page: Page, data: object) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const mockedListDocs = async (page: Page, data: object[] = []) => {
|
export const mockedListDocs = async (page: Page, data: object[] = []) => {
|
||||||
await page.route('**/documents/**/', async (route) => {
|
await page.route(/\**\/documents\/\**/, async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
if (request.method().includes('GET') && request.url().includes('page=')) {
|
if (request.method().includes('GET') && request.url().includes('page=')) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
@@ -276,6 +287,7 @@ export const expectLoginPage = async (page: Page) =>
|
|||||||
).toBeVisible({
|
).toBeVisible({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// language helper
|
// language helper
|
||||||
export const TestLanguage = {
|
export const TestLanguage = {
|
||||||
English: {
|
English: {
|
||||||
@@ -299,7 +311,7 @@ export async function waitForLanguageSwitch(
|
|||||||
page: Page,
|
page: Page,
|
||||||
lang: TestLanguageValue,
|
lang: TestLanguageValue,
|
||||||
) {
|
) {
|
||||||
await page.route('**/api/v1.0/users/**', async (route, request) => {
|
await page.route(/\**\/api\/v1.0\/users\/\**/, async (route, request) => {
|
||||||
if (request.method().includes('PATCH')) {
|
if (request.method().includes('PATCH')) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
json: {
|
json: {
|
||||||
@@ -325,3 +337,19 @@ export async function waitForLanguageSwitch(
|
|||||||
|
|
||||||
await page.getByRole('menuitem', { name: lang.label }).click();
|
await page.getByRole('menuitem', { name: lang.label }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const clickInEditorMenu = async (page: Page, textButton: string) => {
|
||||||
|
await page.getByRole('button', { name: 'Open the document options' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: textButton }).click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clickInGridMenu = async (
|
||||||
|
page: Page,
|
||||||
|
row: Locator,
|
||||||
|
textButton: string,
|
||||||
|
) => {
|
||||||
|
await row
|
||||||
|
.getByRole('button', { name: /Open the menu of actions for the document/ })
|
||||||
|
.click();
|
||||||
|
await page.getByRole('menuitem', { name: textButton }).click();
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const getEditor = async ({ page }: { page: Page }) => {
|
|||||||
export const openSuggestionMenu = async ({ page }: { page: Page }) => {
|
export const openSuggestionMenu = async ({ page }: { page: Page }) => {
|
||||||
const editor = await getEditor({ page });
|
const editor = await getEditor({ page });
|
||||||
await editor.click();
|
await editor.click();
|
||||||
await page.locator('.bn-block-outer').last().fill('/');
|
await writeInEditor({ page, text: '/' });
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
@@ -22,6 +22,6 @@ export const writeInEditor = async ({
|
|||||||
text: string;
|
text: string;
|
||||||
}) => {
|
}) => {
|
||||||
const editor = await getEditor({ page });
|
const editor = await getEditor({ page });
|
||||||
editor.locator('.bn-block-outer').last().fill(text);
|
await editor.locator('.bn-block-outer .bn-inline-content').last().fill(text);
|
||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { Page, chromium, expect } from '@playwright/test';
|
import { Page, chromium, expect } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BROWSERS,
|
|
||||||
BrowserName,
|
BrowserName,
|
||||||
|
getOtherBrowserName,
|
||||||
keyCloakSignIn,
|
keyCloakSignIn,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
|
|
||||||
export type Role = 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader';
|
export type Role = 'Administrator' | 'Owner' | 'Editor' | 'Reader';
|
||||||
export type LinkReach = 'Private' | 'Connected' | 'Public';
|
export type LinkReach = 'Private' | 'Connected' | 'Public';
|
||||||
export type LinkRole = 'Reading' | 'Editing';
|
export type LinkRole = 'Reading' | 'Editing';
|
||||||
|
|
||||||
export const addNewMember = async (
|
export const addNewMember = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
index: number,
|
index: number,
|
||||||
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
|
role: Role,
|
||||||
fillText: string = 'user.test',
|
fillText = 'user.test',
|
||||||
) => {
|
) => {
|
||||||
const responsePromiseSearchUser = page.waitForResponse(
|
const responsePromiseSearchUser = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
@@ -38,9 +38,9 @@ export const addNewMember = async (
|
|||||||
await page.getByRole('option', { name: users[index].email }).click();
|
await page.getByRole('option', { name: users[index].email }).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
await page.getByLabel('doc-role-dropdown').click();
|
await page.getByTestId('doc-role-dropdown').click();
|
||||||
await page.getByRole('menuitem', { name: role }).click();
|
await page.getByRole('menuitem', { name: role }).click();
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByTestId('doc-share-invite-button').click();
|
||||||
|
|
||||||
return users[index].email;
|
return users[index].email;
|
||||||
};
|
};
|
||||||
@@ -74,7 +74,7 @@ export const updateRoleUser = async (
|
|||||||
const list = page.getByTestId('doc-share-quick-search');
|
const list = page.getByTestId('doc-share-quick-search');
|
||||||
|
|
||||||
const currentUser = list.getByTestId(`doc-share-member-row-${email}`);
|
const currentUser = list.getByTestId(`doc-share-member-row-${email}`);
|
||||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
|
||||||
await currentUserRole.click();
|
await currentUserRole.click();
|
||||||
await page.getByRole('menuitem', { name: role }).click();
|
await page.getByRole('menuitem', { name: role }).click();
|
||||||
await list.click();
|
await list.click();
|
||||||
@@ -88,21 +88,30 @@ export const updateRoleUser = async (
|
|||||||
* @param docTitle The title of the document (optional).
|
* @param docTitle The title of the document (optional).
|
||||||
* @returns An object containing the other browser, context, and page.
|
* @returns An object containing the other browser, context, and page.
|
||||||
*/
|
*/
|
||||||
|
type ConnectOtherUserToDocParams = {
|
||||||
|
docUrl: string;
|
||||||
|
docTitle?: string;
|
||||||
|
withoutSignIn?: boolean;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
otherBrowserName: BrowserName;
|
||||||
|
browserName?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
browserName: BrowserName;
|
||||||
|
otherBrowserName?: never;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const connectOtherUserToDoc = async ({
|
export const connectOtherUserToDoc = async ({
|
||||||
browserName,
|
browserName,
|
||||||
docUrl,
|
docUrl,
|
||||||
docTitle,
|
docTitle,
|
||||||
|
otherBrowserName: _otherBrowserName,
|
||||||
withoutSignIn,
|
withoutSignIn,
|
||||||
}: {
|
}: ConnectOtherUserToDocParams) => {
|
||||||
browserName: BrowserName;
|
const otherBrowserName =
|
||||||
docUrl: string;
|
_otherBrowserName || getOtherBrowserName(browserName);
|
||||||
docTitle?: string;
|
|
||||||
withoutSignIn?: boolean;
|
|
||||||
}) => {
|
|
||||||
const otherBrowserName = BROWSERS.find((b) => b !== browserName);
|
|
||||||
if (!otherBrowserName) {
|
|
||||||
throw new Error('No alternative browser found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const otherBrowser = await chromium.launch({ headless: true });
|
const otherBrowser = await chromium.launch({ headless: true });
|
||||||
const otherContext = await otherBrowser.newContext({
|
const otherContext = await otherBrowser.newContext({
|
||||||
@@ -118,12 +127,16 @@ export const connectOtherUserToDoc = async ({
|
|||||||
await otherPage.goto(docUrl);
|
await otherPage.goto(docUrl);
|
||||||
|
|
||||||
if (!withoutSignIn) {
|
if (!withoutSignIn) {
|
||||||
await otherPage
|
const loginFromApp = otherPage
|
||||||
.getByRole('main', { name: 'Main content' })
|
.getByRole('main', { name: 'Main content' })
|
||||||
.getByLabel('Login')
|
.getByLabel('Login');
|
||||||
.click({
|
const loginFromHome = otherPage.getByRole('button', {
|
||||||
timeout: 15000,
|
name: 'Start Writing',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await loginFromApp.or(loginFromHome).first().click({
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
await keyCloakSignIn(otherPage, otherBrowserName, false);
|
||||||
}
|
}
|
||||||
@@ -159,7 +172,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
|
|||||||
...json,
|
...json,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await page.route('**/invitations/**/', async (route) => {
|
await page.route(/.*\/invitations\/.*/, async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
if (
|
if (
|
||||||
request.method().includes('GET') &&
|
request.method().includes('GET') &&
|
||||||
@@ -180,7 +193,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await page.route(
|
await page.route(
|
||||||
'**/invitations/120ec765-43af-4602-83eb-7f4e1224548a/**/',
|
/.*\/invitations\/120ec765-43af-4602-83eb-7f4e1224548a\/.*/,
|
||||||
async (route) => {
|
async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
if (request.method().includes('DELETE')) {
|
if (request.method().includes('DELETE')) {
|
||||||
@@ -195,7 +208,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const mockedAccesses = async (page: Page, json?: object) => {
|
export const mockedAccesses = async (page: Page, json?: object) => {
|
||||||
await page.route('**/accesses/**/', async (route) => {
|
await page.route(/.*\/accesses\/.*/, async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Page, expect } from '@playwright/test';
|
import { Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BrowserName,
|
||||||
randomName,
|
randomName,
|
||||||
updateDocTitle,
|
updateDocTitle,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
@@ -9,9 +10,9 @@ import {
|
|||||||
|
|
||||||
export const createRootSubPage = async (
|
export const createRootSubPage = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
browserName: string,
|
browserName: BrowserName,
|
||||||
docName: string,
|
docName: string,
|
||||||
isMobile: boolean = false,
|
isMobile = false,
|
||||||
) => {
|
) => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
await page
|
await page
|
||||||
@@ -67,6 +68,63 @@ export const clickOnAddRootSubPage = async (page: Page) => {
|
|||||||
await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
|
await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addChild = async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
docParent,
|
||||||
|
docName,
|
||||||
|
}: {
|
||||||
|
page: Page;
|
||||||
|
browserName: BrowserName;
|
||||||
|
docParent: string;
|
||||||
|
docName: string;
|
||||||
|
}) => {
|
||||||
|
let item = page.getByTestId('doc-tree-root-item');
|
||||||
|
|
||||||
|
const isParent = await item
|
||||||
|
.filter({
|
||||||
|
hasText: docParent,
|
||||||
|
})
|
||||||
|
.first()
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if (!isParent) {
|
||||||
|
const items = page.getByRole('treeitem');
|
||||||
|
|
||||||
|
item = items
|
||||||
|
.filter({
|
||||||
|
hasText: docParent,
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
await item.hover();
|
||||||
|
await item.getByTestId('doc-tree-item-actions-add-child').click();
|
||||||
|
|
||||||
|
const [name] = randomName(docName, browserName, 1);
|
||||||
|
await updateDocTitle(page, name);
|
||||||
|
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTreeRow = async (page: Page, title: string) => {
|
||||||
|
const docTree = page.getByTestId('doc-tree');
|
||||||
|
const row = docTree
|
||||||
|
.getByRole('treeitem')
|
||||||
|
.filter({
|
||||||
|
hasText: title,
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
|
||||||
|
return row;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
|
||||||
|
await page.getByRole('link', { name: /Open root document/ }).click();
|
||||||
|
};
|
||||||
|
|
||||||
export const navigateToPageFromTree = async ({
|
export const navigateToPageFromTree = async ({
|
||||||
page,
|
page,
|
||||||
title,
|
title,
|
||||||
@@ -75,6 +133,10 @@ export const navigateToPageFromTree = async ({
|
|||||||
title: string;
|
title: string;
|
||||||
}) => {
|
}) => {
|
||||||
const docTree = page.getByTestId('doc-tree');
|
const docTree = page.getByTestId('doc-tree');
|
||||||
await docTree.getByText(title).click();
|
await docTree
|
||||||
|
.getByText(title, {
|
||||||
|
exact: true,
|
||||||
|
})
|
||||||
|
.click();
|
||||||
await verifyDocName(page, title);
|
await verifyDocName(page, title);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-e2e",
|
"name": "app-e2e",
|
||||||
"version": "3.7.0",
|
"version": "3.10.0",
|
||||||
"repository": "https://github.com/suitenumerique/docs",
|
"repository": "https://github.com/suitenumerique/docs",
|
||||||
"author": "DINUM",
|
"author": "DINUM",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.55.0",
|
"@playwright/test": "1.56.1",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/pdf-parse": "1.1.5",
|
"@types/pdf-parse": "1.1.5",
|
||||||
"eslint-plugin-docs": "*",
|
"eslint-plugin-docs": "*",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"convert-stream": "1.0.2",
|
"convert-stream": "1.0.2",
|
||||||
"pdf-parse": "1.1.1"
|
"pdf-parse": "2.4.5"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22"
|
"packageManager": "yarn@1.22.22"
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/frontend/apps/e2e/print-datauris.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//utilitary script to print the datauris of the targeted assets
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ASSETS_ROOT = path.resolve(__dirname, '__tests__/app-impress/assets');
|
||||||
|
|
||||||
|
function saveDataUrl(file, mime, outName) {
|
||||||
|
const abs = path.join(ASSETS_ROOT, file);
|
||||||
|
const base64 = fs.readFileSync(abs).toString('base64');
|
||||||
|
const dataUrl = `data:${mime};base64,${base64}`;
|
||||||
|
const outPath = path.join(ASSETS_ROOT, outName);
|
||||||
|
fs.writeFileSync(outPath, dataUrl, 'utf8');
|
||||||
|
console.log(`Wrote ${outName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PNG
|
||||||
|
saveDataUrl('panopng.png', 'image/png', 'pano-png-dataurl.txt');
|
||||||
|
|
||||||
|
// JPG
|
||||||
|
saveDataUrl('panojpg.jpeg', 'image/jpeg', 'pano-jpg-dataurl.txt');
|
||||||
|
|
||||||
|
// SVG
|
||||||
|
const svgPath = path.join(ASSETS_ROOT, 'test.svg');
|
||||||
|
const svgText = fs.readFileSync(svgPath, 'utf8');
|
||||||
|
const svgDataUrl =
|
||||||
|
'data:image/svg+xml;base64,' + Buffer.from(svgText).toString('base64');
|
||||||
|
fs.writeFileSync(path.join(ASSETS_ROOT, 'test-svg-dataurl.txt'), svgDataUrl, 'utf8');
|
||||||
|
console.log('Wrote test-svg-dataurl.txt');
|
||||||
3
src/frontend/apps/impress/.stylelintignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
out/**/*
|
||||||
|
public/assets/fonts/**/*
|
||||||
|
node_modules/**/*
|
||||||
@@ -1,57 +1,25 @@
|
|||||||
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
|
import { cunninghamConfig as tokens } from '@gouvfr-lasuite/ui-kit';
|
||||||
|
import { defaultTokens } from '@openfun/cunningham-react';
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
|
||||||
const customColors = {
|
// Uikit does not provide the full list of tokens.
|
||||||
'primary-action': '#1212FF',
|
// To be able to override correctly, we need to merge with the default tokens.
|
||||||
'primary-bg': '#FAFAFA',
|
let mergedColors = merge(
|
||||||
'primary-focus': '#0A76F6',
|
defaultTokens.globals.colors,
|
||||||
'secondary-icon': 'var(--c--theme--colors--primary-text)',
|
tokens.themes.default.globals.colors,
|
||||||
'blue-400': '#7AB1E8',
|
);
|
||||||
'blue-500': '#417DC4',
|
|
||||||
'blue-600': '#3558A2',
|
mergedColors = {
|
||||||
'brown-400': '#E6BE92',
|
...mergedColors,
|
||||||
'brown-500': '#BD987A',
|
'logo-1': '#2845C1',
|
||||||
'brown-600': '#745B47',
|
|
||||||
'cyan-400': '#34BAB5',
|
|
||||||
'cyan-500': '#009099',
|
|
||||||
'cyan-600': '#006A6F',
|
|
||||||
'gold-400': '#FFCA00',
|
|
||||||
'gold-500': '#C3992A',
|
|
||||||
'gold-600': '#695240',
|
|
||||||
'green-400': '#34CB6A',
|
|
||||||
'green-500': '#00A95F',
|
|
||||||
'green-600': '#297254',
|
|
||||||
'olive-400': '#99C221',
|
|
||||||
'olive-500': '#68A532',
|
|
||||||
'olive-600': '#447049',
|
|
||||||
'orange-400': '#FF732C',
|
|
||||||
'orange-500': '#E4794A',
|
|
||||||
'orange-600': '#755348',
|
|
||||||
'pink-400': '#FFB7AE',
|
|
||||||
'pink-500': '#E18B76',
|
|
||||||
'pink-600': '#8D533E',
|
|
||||||
'purple-400': '#CE70CC',
|
|
||||||
'purple-500': '#A558A0',
|
|
||||||
'purple-600': '#6E445A',
|
|
||||||
'yellow-400': '#D8C634',
|
|
||||||
'yellow-500': '#B7A73F',
|
|
||||||
'yellow-600': '#66673D',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tokens.themes.default.theme = {
|
tokens.themes.default.globals = {
|
||||||
...tokens.themes.default.theme,
|
...tokens.themes.default.globals,
|
||||||
...{
|
...{
|
||||||
logo: {
|
colors: mergedColors,
|
||||||
src: '',
|
|
||||||
alt: '',
|
|
||||||
widthHeader: '',
|
|
||||||
widthFooter: '',
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
...tokens.themes.default.theme.colors,
|
|
||||||
...customColors,
|
|
||||||
},
|
|
||||||
font: {
|
font: {
|
||||||
...tokens.themes.default.theme.font,
|
...tokens.themes.default.globals.font,
|
||||||
families: {
|
families: {
|
||||||
base: 'sans-serif',
|
base: 'sans-serif',
|
||||||
accent: 'sans-serif',
|
accent: 'sans-serif',
|
||||||
@@ -63,49 +31,50 @@ tokens.themes.default.theme = {
|
|||||||
tokens.themes.default.components = {
|
tokens.themes.default.components = {
|
||||||
...tokens.themes.default.components,
|
...tokens.themes.default.components,
|
||||||
...{
|
...{
|
||||||
|
logo: {
|
||||||
|
src: '',
|
||||||
|
alt: '',
|
||||||
|
widthHeader: '',
|
||||||
|
widthFooter: '',
|
||||||
|
},
|
||||||
'la-gaufre': false,
|
'la-gaufre': false,
|
||||||
'home-proconnect': false,
|
'home-proconnect': false,
|
||||||
'image-system-filter': '',
|
icon: {
|
||||||
|
src: '/assets/icon-docs.svg',
|
||||||
|
width: '32px',
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
favicon: {
|
favicon: {
|
||||||
ico: '/assets/favicon-light.ico',
|
|
||||||
'png-light': '/assets/favicon-light.png',
|
'png-light': '/assets/favicon-light.png',
|
||||||
'png-dark': '/assets/favicon-dark.png',
|
'png-dark': '/assets/favicon-dark.png',
|
||||||
},
|
},
|
||||||
button: {
|
|
||||||
...tokens.themes.default.components.button,
|
|
||||||
primary: {
|
|
||||||
...tokens.themes.default.components.button.primary,
|
|
||||||
...{
|
|
||||||
'background--disabled': 'var(--c--theme--colors--greyscale-100)',
|
|
||||||
},
|
|
||||||
disabled: 'var(--c--theme--colors--greyscale-400)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const dsfrTheme = {
|
const dsfrTheme = {
|
||||||
dsfr: {
|
dsfr: {
|
||||||
theme: {
|
globals: {
|
||||||
colors: {
|
font: {
|
||||||
'secondary-icon': '#C9191E',
|
families: {
|
||||||
|
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||||
|
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
logo: {
|
logo: {
|
||||||
src: '/assets/logo-gouv.svg',
|
src: '/assets/logo-gouv.svg',
|
||||||
widthHeader: '110px',
|
widthHeader: '110px',
|
||||||
widthFooter: '220px',
|
widthFooter: '220px',
|
||||||
alt: 'Gouvernement Logo',
|
alt: 'Gouvernement Logo',
|
||||||
},
|
},
|
||||||
font: {
|
|
||||||
families: {
|
|
||||||
base: 'Marianne',
|
|
||||||
accent: 'Marianne',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
'la-gaufre': true,
|
'la-gaufre': true,
|
||||||
'home-proconnect': true,
|
'home-proconnect': true,
|
||||||
|
icon: {
|
||||||
|
src: '/assets/icon-docs-dsfr.svg',
|
||||||
|
width: '32px',
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
favicon: {
|
favicon: {
|
||||||
ico: '/assets/favicon-dsfr.ico',
|
ico: '/assets/favicon-dsfr.ico',
|
||||||
'png-light': '/assets/favicon-dsfr.png',
|
'png-light': '/assets/favicon-dsfr.png',
|
||||||
@@ -117,131 +86,296 @@ const dsfrTheme = {
|
|||||||
|
|
||||||
const genericTheme = {
|
const genericTheme = {
|
||||||
generic: {
|
generic: {
|
||||||
theme: {
|
globals: {
|
||||||
colors: {
|
colors: {
|
||||||
'primary-action': '#206EBD',
|
'brand-050': '#EEF1FA',
|
||||||
'primary-focus': '#1E64BF',
|
'brand-100': '#DDE2F5',
|
||||||
'primary-text': '#2E2C28',
|
'brand-150': '#CED3F1',
|
||||||
'primary-050': '#F8F8F7',
|
'brand-200': '#BEC5F0',
|
||||||
'primary-100': '#F0EFEC',
|
'brand-250': '#AFB5F1',
|
||||||
'primary-150': '#F4F4FD',
|
'brand-300': '#A0A5F6',
|
||||||
'primary-200': '#E8E7E4',
|
'brand-350': '#8F94FD',
|
||||||
'primary-300': '#CFCDC9',
|
'brand-400': '#8184FC',
|
||||||
'primary-400': '#979592',
|
'brand-450': '#7576EE',
|
||||||
'primary-500': '#82807D',
|
'brand-500': '#6969DF',
|
||||||
'primary-600': '#3F3D39',
|
'brand-550': '#5E5CD0',
|
||||||
'primary-700': '#2E2C28',
|
'brand-600': '#534FC2',
|
||||||
'primary-800': '#302E29',
|
'brand-650': '#4844AD',
|
||||||
'primary-900': '#282622',
|
'brand-700': '#3E3B98',
|
||||||
'primary-950': '#201F1C',
|
'brand-750': '#36347D',
|
||||||
'secondary-text': '#fff',
|
'brand-800': '#2D2F5F',
|
||||||
'secondary-50': '#F4F7FA',
|
'brand-850': '#262848',
|
||||||
'secondary-100': '#D7E3EE',
|
'brand-900': '#1C1E32',
|
||||||
'secondary-200': '#B8CCE1',
|
'brand-950': '#11131F',
|
||||||
'secondary-300': '#99B4D3',
|
'gray-000': '#FFFFFF',
|
||||||
'secondary-400': '#7595BE',
|
'gray-025': '#F8F8F9',
|
||||||
'secondary-500': '#5874A0',
|
'gray-050': '#F0F0F3',
|
||||||
'secondary-600': '#3A5383',
|
'gray-100': '#E2E2EA',
|
||||||
'secondary-700': '#1E3462',
|
'gray-150': '#D3D4E0',
|
||||||
'secondary-800': '#091B41',
|
'gray-200': '#C5C6D5',
|
||||||
'secondary-900': '#08183B',
|
'gray-250': '#B7B7CB',
|
||||||
'secondary-950': '#071636',
|
'gray-300': '#A9A9BF',
|
||||||
'greyscale-text': '#3C3B38',
|
'gray-350': '#9C9CB2',
|
||||||
'greyscale-000': '#fff',
|
'gray-400': '#8F8FA4',
|
||||||
'greyscale-050': '#F8F7F7',
|
'gray-450': '#828297',
|
||||||
'greyscale-100': '#F3F3F2',
|
'gray-500': '#75758A',
|
||||||
'greyscale-200': '#ECEBEA',
|
'gray-550': '#69697D',
|
||||||
'greyscale-250': '#E4E3E2',
|
'gray-600': '#5D5D70',
|
||||||
'greyscale-300': '#D3D2CF',
|
'gray-650': '#515164',
|
||||||
'greyscale-350': '#eee',
|
'gray-700': '#454558',
|
||||||
'greyscale-400': '#96948E',
|
'gray-750': '#3A3A4C',
|
||||||
'greyscale-500': '#817E77',
|
'gray-800': '#2F303D',
|
||||||
'greyscale-600': '#6A6862',
|
'gray-850': '#25252F',
|
||||||
'greyscale-700': '#3C3B38',
|
'gray-900': '#1B1B23',
|
||||||
'greyscale-750': '#383632',
|
'gray-950': '#111114',
|
||||||
'greyscale-800': '#2D2B27',
|
'gray-1000': '#000000',
|
||||||
'greyscale-900': '#262522',
|
'info-050': '#EAF2F9',
|
||||||
'greyscale-950': '#201F1C',
|
'info-100': '#D5E4F3',
|
||||||
'greyscale-1000': '#181714',
|
'info-150': '#BFD7F0',
|
||||||
'success-text': '#234935',
|
'info-200': '#A7CAEE',
|
||||||
'success-50': '#F3FBF5',
|
'info-250': '#8DBDEF',
|
||||||
'success-100': '#E4F7EA',
|
'info-300': '#6EB0F2',
|
||||||
'success-200': '#CAEED4',
|
'info-350': '#50A2F5',
|
||||||
'success-300': '#A0E0B5',
|
'info-400': '#3593F4',
|
||||||
'success-400': '#6CC88C',
|
'info-450': '#1185ED',
|
||||||
'success-500': '#6CC88C',
|
'info-500': '#0077DE',
|
||||||
'success-600': '#358D5C',
|
'info-550': '#0069CF',
|
||||||
'success-700': '#2D704B',
|
'info-600': '#005BC0',
|
||||||
'success-800': '#28583F',
|
'info-650': '#0D4EAA',
|
||||||
'success-900': '#234935',
|
'info-700': '#124394',
|
||||||
'success-950': '#0F281B',
|
'info-750': '#163878',
|
||||||
'info-text': '#212445',
|
'info-800': '#192F5A',
|
||||||
'info-50': '#F2F6FB',
|
'info-850': '#192541',
|
||||||
'info-100': '#E2E9F5',
|
'info-900': '#141B2D',
|
||||||
'info-200': '#CCD8EE',
|
'info-950': '#0C111C',
|
||||||
'info-300': '#A9C0E3',
|
'success-050': '#E8F1EA',
|
||||||
'info-400': '#809DD4',
|
'success-100': '#CFE4D4',
|
||||||
'info-500': '#617BC7',
|
'success-150': '#BAD9C1',
|
||||||
'info-600': '#4A5CBF',
|
'success-200': '#A2CFAD',
|
||||||
'info-700': '#3E49B2',
|
'success-250': '#86C597',
|
||||||
'info-800': '#353C8F',
|
'success-300': '#6CBA83',
|
||||||
'info-900': '#303771',
|
'success-350': '#4FB070',
|
||||||
'info-950': '#212445',
|
'success-400': '#40A363',
|
||||||
'warning-text': '#D97C3A',
|
'success-450': '#309556',
|
||||||
'warning-50': '#FDF7F1',
|
'success-500': '#1E884A',
|
||||||
'warning-100': '#FBEDDC',
|
'success-550': '#027B3E',
|
||||||
'warning-200': '#F5D9B9',
|
'success-600': '#016D31',
|
||||||
'warning-300': '#EDBE8C',
|
'success-650': '#006024',
|
||||||
'warning-400': '#E2985C',
|
'success-700': '#005317',
|
||||||
'warning-500': '#D97C3A',
|
'success-750': '#0D4511',
|
||||||
'warning-600': '#C96330',
|
'success-800': '#11380E',
|
||||||
'warning-700': '#A34B32',
|
'success-850': '#132A11',
|
||||||
'warning-800': '#813B2C',
|
'success-900': '#101E0F',
|
||||||
'warning-900': '#693327',
|
'success-950': '#091209',
|
||||||
'warning-950': '#381713',
|
'warning-050': '#F8F0E9',
|
||||||
'danger-action': '#C0182A',
|
'warning-100': '#F1E0D3',
|
||||||
'danger-text': '#FFF',
|
'warning-150': '#ECD0BC',
|
||||||
'danger-050': '#FDF5F4',
|
'warning-200': '#E8C0A4',
|
||||||
'danger-100': '#FBEBE8',
|
'warning-250': '#E8AE8A',
|
||||||
'danger-200': '#F9E0DC',
|
'warning-300': '#EB9970',
|
||||||
'danger-300': '#F3C3BD',
|
'warning-350': '#E98456',
|
||||||
'danger-400': '#E26552',
|
'warning-400': '#E57036',
|
||||||
'danger-500': '#C91F00',
|
'warning-450': '#DA5E18',
|
||||||
'danger-600': '#A71901',
|
'warning-500': '#CB5000',
|
||||||
'danger-700': '#562C2B',
|
'warning-550': '#BC4200',
|
||||||
'danger-800': '#392425',
|
'warning-600': '#AD3300',
|
||||||
'danger-900': '#311F20',
|
'warning-650': '#9E2300',
|
||||||
'danger-950': '#2A191A',
|
'warning-700': '#882011',
|
||||||
'blue-400': '#8BAECC',
|
'warning-750': '#731E16',
|
||||||
'blue-500': '#567AA2',
|
'warning-800': '#58201A',
|
||||||
'blue-600': '#455784',
|
'warning-850': '#401D18',
|
||||||
'brown-400': '#E4C090',
|
'warning-900': '#2E1714',
|
||||||
'brown-500': '#BA9977',
|
'warning-950': '#1D0F0D',
|
||||||
'brown-600': '#735C45',
|
'error-050': '#F9EFEC',
|
||||||
'cyan-400': '#5CBEC9',
|
'error-100': '#F4DFD9',
|
||||||
'cyan-500': '#43A1B3',
|
'error-150': '#F0CEC6',
|
||||||
'cyan-600': '#39809B',
|
'error-200': '#EEBCB2',
|
||||||
'gold-400': '#ECBF50',
|
'error-250': '#EEA99D',
|
||||||
'gold-500': '#DFA038',
|
'error-300': '#EF9486',
|
||||||
'gold-600': '#C17B31',
|
'error-350': '#F37C6E',
|
||||||
'green-400': '#5DBD9A',
|
'error-400': '#F65F53',
|
||||||
'green-500': '#3AA183',
|
'error-450': '#F0463D',
|
||||||
'green-600': '#2A816D',
|
'error-500': '#E82322',
|
||||||
'olive-400': '#AFD662',
|
'error-550': '#D7010E',
|
||||||
'olive-500': '#90BB4B',
|
'error-600': '#C00100',
|
||||||
'olive-600': '#6E9441',
|
'error-650': '#AA0000',
|
||||||
'orange-400': '#E2985C',
|
'error-700': '#910C06',
|
||||||
'orange-500': '#D97C3A',
|
'error-750': '#731E16',
|
||||||
'orange-600': '#C96330',
|
'error-800': '#58201A',
|
||||||
'pink-400': '#BE8FC8',
|
'error-850': '#401D18',
|
||||||
'pink-500': '#A563B1',
|
'error-900': '#2E1714',
|
||||||
'pink-600': '#8B44A5',
|
'error-950': '#1D0F0D',
|
||||||
'purple-400': '#BE8FC8',
|
'red-050': '#FAEFEE',
|
||||||
'purple-500': '#A563B1',
|
'red-100': '#F4DEDD',
|
||||||
'purple-600': '#8B44A5',
|
'red-150': '#F1CDCB',
|
||||||
'yellow-400': '#EDC947',
|
'red-200': '#EFBBBA',
|
||||||
'yellow-500': '#DBB13A',
|
'red-250': '#EEA8A8',
|
||||||
'yellow-600': '#B88A34',
|
'red-300': '#F09394',
|
||||||
|
'red-350': '#F37B7E',
|
||||||
|
'red-400': '#EF6569',
|
||||||
|
'red-450': '#E94A55',
|
||||||
|
'red-500': '#DA3B49',
|
||||||
|
'red-550': '#CA2A3C',
|
||||||
|
'red-600': '#BB1330',
|
||||||
|
'red-650': '#A90021',
|
||||||
|
'red-700': '#910A13',
|
||||||
|
'red-750': '#731E16',
|
||||||
|
'red-800': '#58201A',
|
||||||
|
'red-850': '#411D18',
|
||||||
|
'red-900': '#2E1714',
|
||||||
|
'red-950': '#1D0F0D',
|
||||||
|
'orange-050': '#F8F0E9',
|
||||||
|
'orange-100': '#F1E0D3',
|
||||||
|
'orange-150': '#ECD0BD',
|
||||||
|
'orange-200': '#EABFA6',
|
||||||
|
'orange-250': '#EBAC90',
|
||||||
|
'orange-300': '#EC9772',
|
||||||
|
'orange-350': '#E5845A',
|
||||||
|
'orange-400': '#D6774D',
|
||||||
|
'orange-450': '#C86A40',
|
||||||
|
'orange-500': '#B95D33',
|
||||||
|
'orange-550': '#AB5025',
|
||||||
|
'orange-600': '#9D4315',
|
||||||
|
'orange-650': '#8F3600',
|
||||||
|
'orange-700': '#812900',
|
||||||
|
'orange-750': '#6C2511',
|
||||||
|
'orange-800': '#572017',
|
||||||
|
'orange-850': '#401D18',
|
||||||
|
'orange-900': '#2E1714',
|
||||||
|
'orange-950': '#1D0F0D',
|
||||||
|
'brown-050': '#F6F0E8',
|
||||||
|
'brown-100': '#F1E0D3',
|
||||||
|
'brown-150': '#EBD0BA',
|
||||||
|
'brown-200': '#E2C0A6',
|
||||||
|
'brown-250': '#D4B398',
|
||||||
|
'brown-300': '#C6A58B',
|
||||||
|
'brown-350': '#B8987E',
|
||||||
|
'brown-400': '#AA8B71',
|
||||||
|
'brown-450': '#9D7E65',
|
||||||
|
'brown-500': '#8F7158',
|
||||||
|
'brown-550': '#82654C',
|
||||||
|
'brown-600': '#765841',
|
||||||
|
'brown-650': '#694C35',
|
||||||
|
'brown-700': '#5D412A',
|
||||||
|
'brown-750': '#51361E',
|
||||||
|
'brown-800': '#452A13',
|
||||||
|
'brown-850': '#392008',
|
||||||
|
'brown-900': '#29180A',
|
||||||
|
'brown-950': '#1B0F08',
|
||||||
|
'yellow-050': '#F3F0E7',
|
||||||
|
'yellow-100': '#E9E2CF',
|
||||||
|
'yellow-150': '#E1D4B7',
|
||||||
|
'yellow-200': '#D9C599',
|
||||||
|
'yellow-250': '#D2B677',
|
||||||
|
'yellow-300': '#CAA756',
|
||||||
|
'yellow-350': '#C2972E',
|
||||||
|
'yellow-400': '#B98900',
|
||||||
|
'yellow-450': '#AB7B00',
|
||||||
|
'yellow-500': '#9D6E00',
|
||||||
|
'yellow-550': '#916100',
|
||||||
|
'yellow-600': '#855400',
|
||||||
|
'yellow-650': '#784700',
|
||||||
|
'yellow-700': '#6C3A00',
|
||||||
|
'yellow-750': '#5F2E00',
|
||||||
|
'yellow-800': '#512302',
|
||||||
|
'yellow-850': '#3E1D10',
|
||||||
|
'yellow-900': '#2D1711',
|
||||||
|
'yellow-950': '#1D0F0D',
|
||||||
|
'green-050': '#E6F1E9',
|
||||||
|
'green-100': '#CFE4D5',
|
||||||
|
'green-150': '#B8D8C1',
|
||||||
|
'green-200': '#A0CFAE',
|
||||||
|
'green-250': '#84C59A',
|
||||||
|
'green-300': '#65BA86',
|
||||||
|
'green-350': '#45B173',
|
||||||
|
'green-400': '#23A562',
|
||||||
|
'green-450': '#029755',
|
||||||
|
'green-500': '#008948',
|
||||||
|
'green-550': '#017B3B',
|
||||||
|
'green-600': '#006E2E',
|
||||||
|
'green-650': '#006022',
|
||||||
|
'green-700': '#005314',
|
||||||
|
'green-750': '#0D4510',
|
||||||
|
'green-800': '#11380E',
|
||||||
|
'green-850': '#132A11',
|
||||||
|
'green-900': '#101E0F',
|
||||||
|
'green-950': '#091209',
|
||||||
|
'blue1-050': '#EBF1F9',
|
||||||
|
'blue1-100': '#D6E4F4',
|
||||||
|
'blue1-150': '#C1D7F0',
|
||||||
|
'blue1-200': '#AACAEF',
|
||||||
|
'blue1-250': '#8FBCEF',
|
||||||
|
'blue1-300': '#7CAFEB',
|
||||||
|
'blue1-350': '#68A1E4',
|
||||||
|
'blue1-400': '#5B94D6',
|
||||||
|
'blue1-450': '#4E86C7',
|
||||||
|
'blue1-500': '#4279B9',
|
||||||
|
'blue1-550': '#356CAC',
|
||||||
|
'blue1-600': '#28609E',
|
||||||
|
'blue1-650': '#1B5390',
|
||||||
|
'blue1-700': '#0B4783',
|
||||||
|
'blue1-750': '#0F3C6E',
|
||||||
|
'blue1-800': '#133059',
|
||||||
|
'blue1-850': '#152641',
|
||||||
|
'blue1-900': '#121C2D',
|
||||||
|
'blue1-950': '#0B111C',
|
||||||
|
'blue2-050': '#E7F3F4',
|
||||||
|
'blue2-100': '#CEE7E9',
|
||||||
|
'blue2-150': '#B2DCE0',
|
||||||
|
'blue2-200': '#91D1D7',
|
||||||
|
'blue2-250': '#68C7D0',
|
||||||
|
'blue2-300': '#43BBC5',
|
||||||
|
'blue2-350': '#00AFBA',
|
||||||
|
'blue2-400': '#01A0AA',
|
||||||
|
'blue2-450': '#00929D',
|
||||||
|
'blue2-500': '#00848F',
|
||||||
|
'blue2-550': '#007682',
|
||||||
|
'blue2-600': '#016874',
|
||||||
|
'blue2-650': '#005B67',
|
||||||
|
'blue2-700': '#004E5A',
|
||||||
|
'blue2-750': '#00424E',
|
||||||
|
'blue2-800': '#003642',
|
||||||
|
'blue2-850': '#002A38',
|
||||||
|
'blue2-900': '#061E28',
|
||||||
|
'blue2-950': '#071219',
|
||||||
|
'purple-050': '#F7F0F6',
|
||||||
|
'purple-100': '#EEE0EE',
|
||||||
|
'purple-150': '#E7D1E7',
|
||||||
|
'purple-200': '#DBBFE4',
|
||||||
|
'purple-250': '#D3AEE2',
|
||||||
|
'purple-300': '#CB99E1',
|
||||||
|
'purple-350': '#C188D9',
|
||||||
|
'purple-400': '#B47BCB',
|
||||||
|
'purple-450': '#A66EBD',
|
||||||
|
'purple-500': '#9961AF',
|
||||||
|
'purple-550': '#8B55A1',
|
||||||
|
'purple-600': '#7E4894',
|
||||||
|
'purple-650': '#723C87',
|
||||||
|
'purple-700': '#633376',
|
||||||
|
'purple-750': '#552A65',
|
||||||
|
'purple-800': '#452551',
|
||||||
|
'purple-850': '#35213D',
|
||||||
|
'purple-900': '#261A2C',
|
||||||
|
'purple-950': '#17111C',
|
||||||
|
'pink-050': '#F8EFF4',
|
||||||
|
'pink-100': '#F0DFEA',
|
||||||
|
'pink-150': '#EACEDF',
|
||||||
|
'pink-200': '#E9BBD1',
|
||||||
|
'pink-250': '#E9A7C2',
|
||||||
|
'pink-300': '#E095B4',
|
||||||
|
'pink-350': '#D685A8',
|
||||||
|
'pink-400': '#C7799B',
|
||||||
|
'pink-450': '#B86C8D',
|
||||||
|
'pink-500': '#AA5F80',
|
||||||
|
'pink-550': '#9C5374',
|
||||||
|
'pink-600': '#8E4767',
|
||||||
|
'pink-650': '#813B5B',
|
||||||
|
'pink-700': '#732E4F',
|
||||||
|
'pink-750': '#632643',
|
||||||
|
'pink-800': '#521F38',
|
||||||
|
'pink-850': '#3E1C2B',
|
||||||
|
'pink-900': '#2D171F',
|
||||||
|
'pink-950': '#1C0E12',
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
families: {
|
families: {
|
||||||
@@ -250,18 +384,6 @@ const genericTheme = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
button: {
|
|
||||||
primary: {
|
|
||||||
background: {
|
|
||||||
'color-hover': 'var(--c--theme--colors--primary-focus)',
|
|
||||||
'color-active': 'var(--c--theme--colors--primary-focus)',
|
|
||||||
'color-focus': 'var(--c--theme--colors--primary-focus)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'image-system-filter': 'saturate(0.2)',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-impress",
|
"name": "app-impress",
|
||||||
"version": "3.7.0",
|
"version": "3.10.0",
|
||||||
"repository": "https://github.com/suitenumerique/docs",
|
"repository": "https://github.com/suitenumerique/docs",
|
||||||
"author": "DINUM",
|
"author": "DINUM",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -19,46 +19,52 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ag-media/react-pdf-table": "2.0.3",
|
"@ag-media/react-pdf-table": "2.0.3",
|
||||||
"@blocknote/code-block": "0.37.0",
|
"@blocknote/code-block": "0.42.3",
|
||||||
"@blocknote/core": "0.37.0",
|
"@blocknote/core": "0.42.3",
|
||||||
"@blocknote/mantine": "0.37.0",
|
"@blocknote/mantine": "0.42.3",
|
||||||
"@blocknote/react": "0.37.0",
|
"@blocknote/react": "0.42.3",
|
||||||
"@blocknote/xl-docx-exporter": "0.37.0",
|
"@blocknote/xl-docx-exporter": "0.42.3",
|
||||||
"@blocknote/xl-multi-column": "0.37.0",
|
"@blocknote/xl-multi-column": "0.42.3",
|
||||||
"@blocknote/xl-pdf-exporter": "0.37.0",
|
"@blocknote/xl-odt-exporter": "0.42.3",
|
||||||
|
"@blocknote/xl-pdf-exporter": "0.42.3",
|
||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
"@dnd-kit/modifiers": "9.0.0",
|
"@dnd-kit/modifiers": "9.0.0",
|
||||||
"@emoji-mart/data": "1.2.1",
|
"@emoji-mart/data": "1.2.1",
|
||||||
"@emoji-mart/react": "1.1.1",
|
"@emoji-mart/react": "1.1.1",
|
||||||
"@fontsource-variable/inter": "5.2.8",
|
"@fontsource-variable/inter": "5.2.8",
|
||||||
"@fontsource/material-icons": "5.2.5",
|
"@fontsource-variable/material-symbols-outlined": "5.2.28",
|
||||||
|
"@fontsource/material-icons": "5.2.7",
|
||||||
"@gouvfr-lasuite/integration": "1.0.3",
|
"@gouvfr-lasuite/integration": "1.0.3",
|
||||||
"@gouvfr-lasuite/ui-kit": "0.16.1",
|
"@gouvfr-lasuite/ui-kit": "0.18.0",
|
||||||
"@hocuspocus/provider": "2.15.2",
|
"@hocuspocus/provider": "3.4.0",
|
||||||
"@openfun/cunningham-react": "3.2.3",
|
"@mantine/core": "8.3.6",
|
||||||
"@react-pdf/renderer": "4.3.0",
|
"@mantine/hooks": "8.3.6",
|
||||||
"@sentry/nextjs": "10.11.0",
|
"@openfun/cunningham-react": "4.0.0",
|
||||||
"@tanstack/react-query": "5.87.4",
|
"@react-pdf/renderer": "4.3.1",
|
||||||
|
"@sentry/nextjs": "10.27.0",
|
||||||
|
"@tanstack/react-query": "5.90.6",
|
||||||
|
"@tiptap/extensions": "*",
|
||||||
"canvg": "4.0.3",
|
"canvg": "4.0.3",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"crisp-sdk-web": "1.0.25",
|
"crisp-sdk-web": "1.0.26",
|
||||||
"docx": "9.5.0",
|
"docx": "*",
|
||||||
"emoji-datasource-apple": "16.0.0",
|
"emoji-datasource-apple": "16.0.0",
|
||||||
"emoji-mart": "5.6.0",
|
"emoji-mart": "5.6.0",
|
||||||
"emoji-regex": "10.5.0",
|
"emoji-regex": "10.6.0",
|
||||||
"i18next": "25.5.2",
|
"i18next": "25.6.0",
|
||||||
"i18next-browser-languagedetector": "8.2.0",
|
"i18next-browser-languagedetector": "8.2.0",
|
||||||
"idb": "8.0.3",
|
"idb": "8.0.3",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"luxon": "3.7.2",
|
"luxon": "3.7.2",
|
||||||
"next": "15.5.3",
|
"next": "15.5.4",
|
||||||
"posthog-js": "1.264.2",
|
"posthog-js": "1.284.0",
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-aria-components": "1.12.1",
|
"react-aria-components": "1.13.0",
|
||||||
"react-dom": "*",
|
"react-dom": "*",
|
||||||
"react-i18next": "15.7.3",
|
"react-i18next": "16.3.3",
|
||||||
"react-intersection-observer": "9.16.0",
|
"react-intersection-observer": "10.0.0",
|
||||||
|
"react-resizable-panels": "3.0.6",
|
||||||
"react-select": "5.10.2",
|
"react-select": "5.10.2",
|
||||||
"styled-components": "6.1.19",
|
"styled-components": "6.1.19",
|
||||||
"use-debounce": "10.0.6",
|
"use-debounce": "10.0.6",
|
||||||
@@ -68,9 +74,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "8.1.0",
|
"@svgr/webpack": "8.1.0",
|
||||||
"@tanstack/react-query-devtools": "5.87.4",
|
"@tanstack/react-query-devtools": "5.90.2",
|
||||||
"@testing-library/dom": "10.4.1",
|
"@testing-library/dom": "10.4.1",
|
||||||
"@testing-library/jest-dom": "6.8.0",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
"@testing-library/react": "16.3.0",
|
"@testing-library/react": "16.3.0",
|
||||||
"@testing-library/user-event": "14.6.1",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@types/lodash": "4.17.20",
|
"@types/lodash": "4.17.20",
|
||||||
@@ -78,22 +84,22 @@
|
|||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"@types/react-dom": "*",
|
"@types/react-dom": "*",
|
||||||
"@vitejs/plugin-react": "5.0.2",
|
"@vitejs/plugin-react": "5.1.0",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"cross-env": "10.0.0",
|
"cross-env": "10.1.0",
|
||||||
"dotenv": "17.2.2",
|
"dotenv": "17.2.3",
|
||||||
"eslint-plugin-docs": "*",
|
"eslint-plugin-docs": "*",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "27.1.0",
|
||||||
"node-fetch": "2.7.0",
|
"node-fetch": "2.7.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"stylelint": "16.24.0",
|
"stylelint": "16.25.0",
|
||||||
"stylelint-config-standard": "39.0.0",
|
"stylelint-config-standard": "39.0.1",
|
||||||
"stylelint-prettier": "5.0.3",
|
"stylelint-prettier": "5.0.3",
|
||||||
"typescript": "*",
|
"typescript": "*",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.2.4",
|
"vitest": "4.0.6",
|
||||||
"webpack": "5.101.3",
|
"webpack": "5.102.1",
|
||||||
"workbox-webpack-plugin": "7.1.0"
|
"workbox-webpack-plugin": "7.1.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22"
|
"packageManager": "yarn@1.22.22"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
12
src/frontend/apps/impress/public/assets/icon-docs-dsfr.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
||||||
|
fill="#C9191E"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
|
||||||
|
fill="#2845C1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,12 +1,4 @@
|
|||||||
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path d="M21.6305 28.8312C22.7983 28.5038 23.9166 27.9062 24.6505 26.8503C25.3749 25.8163 25.5789 24.5047 25.5789 23.2425V4.75099C25.5789 4.42358 25.5611 4.09557 25.5216 3.77148C26.1016 3.99961 26.5486 4.37658 26.8626 4.90239C27.2331 5.50024 27.4184 6.28757 27.4184 7.26435V26.0464C27.4184 27.3684 27.0942 28.3578 26.4458 29.0146C25.7974 29.6714 24.8207 29.9998 23.5155 29.9998H16.4209C16.5889 29.9704 16.7574 29.9401 16.9262 29.909C18.4067 29.6444 19.9713 29.2854 21.6185 28.8346L21.6305 28.8312Z" fill="#4747A3"/>
|
||||||
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58203 25.655V6.8477C4.58203 5.70251 4.88938 4.83519 5.50408 4.24575C6.1272 3.65631 6.95242 3.33212 7.97972 3.27318C9.49542 3.18055 10.9311 3.05425 12.2868 2.89425C13.6425 2.72584 14.9393 2.53217 16.1771 2.31324C17.4234 2.0943 18.6359 1.85011 19.8148 1.58065C21.0274 1.29435 21.9578 1.4375 22.6062 2.0101C23.2546 2.58269 23.5788 3.49632 23.5788 4.75099V23.2425C23.5788 24.3456 23.3893 25.1666 23.0104 25.7055C22.6315 26.2529 21.9915 26.6528 21.0905 26.9054C19.4906 27.3433 17.9833 27.6886 16.5687 27.9412C15.154 28.2022 13.7731 28.4001 12.4258 28.5348C11.0785 28.6696 9.69751 28.7748 8.28286 28.8506C7.11241 28.918 6.20299 28.6738 5.5546 28.118C4.90622 27.5707 4.58203 26.7497 4.58203 25.655ZM9.20865 10.2624C11.0635 10.1444 12.7632 9.96305 14.3075 9.71831C14.6822 9.65722 15.0564 9.5936 15.4291 9.52759C15.8192 9.45851 16.1013 9.11859 16.1013 8.72337C16.1013 8.21154 15.638 7.82609 15.135 7.91189C14.846 7.96118 14.5555 8.00909 14.2635 8.05562C12.7346 8.29923 11.0452 8.47998 9.19523 8.5977C8.91819 8.61558 8.69776 8.70188 8.55608 8.87391C8.42209 9.03661 8.35645 9.23229 8.35645 9.45535C8.35645 9.68212 8.43296 9.87951 8.58568 10.0418L8.58783 10.0439C8.75336 10.2095 8.96369 10.2811 9.20865 10.2624ZM9.20801 14.456C11.0631 14.338 12.763 14.1566 14.3075 13.9119C15.8588 13.6589 17.3936 13.3638 18.9112 13.0266C19.2191 12.9581 19.4498 12.8503 19.5652 12.683C19.6786 12.5221 19.7347 12.3376 19.7347 12.1332C19.7347 11.9026 19.6469 11.704 19.476 11.5426C19.2921 11.3689 19.0348 11.3284 18.7304 11.3911L18.7285 11.3915C17.2823 11.7194 15.794 12.0053 14.2635 12.2492C12.7346 12.4928 11.0452 12.6735 9.19523 12.7913C8.91819 12.8091 8.69776 12.8954 8.55608 13.0675C8.42276 13.2294 8.35645 13.4205 8.35645 13.6363C8.35645 13.8703 8.43209 14.0723 8.58558 14.2354L8.59 14.2396C8.75499 14.3949 8.96316 14.4655 9.20551 14.4562L9.20801 14.456ZM9.20847 18.6494C11.0634 18.5229 12.7631 18.3374 14.3075 18.0927C15.8589 17.8482 17.3934 17.5573 18.9112 17.22C19.2199 17.1514 19.4508 17.0391 19.566 16.8627C19.6783 16.7029 19.7347 16.5233 19.7347 16.3266C19.7347 16.0961 19.6469 15.8974 19.476 15.7361C19.2921 15.5623 19.0348 15.5218 18.7304 15.5845L18.729 15.5848C17.2827 15.9043 15.7942 16.1861 14.2635 16.43C12.7345 16.6736 11.045 16.8586 9.19495 16.9847C8.91804 17.0026 8.69771 17.0889 8.55608 17.2609C8.42276 17.4228 8.35645 17.6139 8.35645 17.8297C8.35645 18.0637 8.43209 18.2658 8.58558 18.4289L8.59 18.433C8.75499 18.5883 8.96316 18.6589 9.20551 18.6496L9.20847 18.6494ZM14.3075 22.257C12.7632 22.5018 11.0635 22.6831 9.20867 22.8012C8.9637 22.8198 8.75337 22.7482 8.58783 22.5826L8.58572 22.5805C8.433 22.4182 8.35645 22.2208 8.35645 21.9941C8.35645 21.771 8.42209 21.5753 8.55608 21.4126C8.69776 21.2406 8.91827 21.1543 9.19531 21.1364C11.0453 21.0187 12.7346 20.838 14.2635 20.5943C14.5555 20.5478 14.846 20.4999 15.135 20.4506C15.638 20.3648 16.1013 20.7503 16.1013 21.2621C16.1013 21.6573 15.8192 21.9972 15.4291 22.0663C15.0564 22.1323 14.6822 22.1959 14.3075 22.257Z" fill="#4747A3"/>
|
||||||
fill="#C9191E"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
|
|
||||||
fill="#000091"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -18,5 +18,5 @@ export const backendUrl = () =>
|
|||||||
* @param apiVersion - The version of the API (defaults to '1.0').
|
* @param apiVersion - The version of the API (defaults to '1.0').
|
||||||
* @returns The full versioned API base URL as a string.
|
* @returns The full versioned API base URL as a string.
|
||||||
*/
|
*/
|
||||||
export const baseApiUrl = (apiVersion: string = '1.0') =>
|
export const baseApiUrl = (apiVersion = '1.0') =>
|
||||||
`${backendUrl()}/api/v${apiVersion}/`;
|
`${backendUrl()}/api/v${apiVersion}/`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
|
||||||
fill="var(--c--theme--colors--secondary-icon)"
|
fill="var(--c--globals--colors--red-600)"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -14,6 +14,7 @@ export interface BoxProps {
|
|||||||
as?: HTMLElementType;
|
as?: HTMLElementType;
|
||||||
$align?: CSSProperties['alignItems'];
|
$align?: CSSProperties['alignItems'];
|
||||||
$background?: CSSProperties['background'];
|
$background?: CSSProperties['background'];
|
||||||
|
$border?: CSSProperties['border'];
|
||||||
$color?: CSSProperties['color'];
|
$color?: CSSProperties['color'];
|
||||||
$css?: string | RuleSet<object>;
|
$css?: string | RuleSet<object>;
|
||||||
$cursor?: CSSProperties['cursor'];
|
$cursor?: CSSProperties['cursor'];
|
||||||
@@ -38,16 +39,32 @@ export interface BoxProps {
|
|||||||
$shrink?: CSSProperties['flexShrink'];
|
$shrink?: CSSProperties['flexShrink'];
|
||||||
$transition?: CSSProperties['transition'];
|
$transition?: CSSProperties['transition'];
|
||||||
$width?: CSSProperties['width'];
|
$width?: CSSProperties['width'];
|
||||||
$wrap?: CSSProperties['flexWrap'];
|
|
||||||
$zIndex?: CSSProperties['zIndex'];
|
$zIndex?: CSSProperties['zIndex'];
|
||||||
|
$wrap?: CSSProperties['flexWrap'];
|
||||||
|
// Theming props
|
||||||
|
$layer?: 'background' | 'content' | 'border';
|
||||||
|
$theme?:
|
||||||
|
| 'brand'
|
||||||
|
| 'error'
|
||||||
|
| 'gray'
|
||||||
|
| 'info'
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| 'neutral'
|
||||||
|
| 'contextual'
|
||||||
|
| 'disabled'
|
||||||
|
| (string & {});
|
||||||
|
$scope?: 'surface' | 'semantic' | 'palette' | (string & {});
|
||||||
|
$variation?: 'primary' | 'secondary' | 'tertiary' | (string & {});
|
||||||
|
$withThemeBG?: boolean;
|
||||||
|
$withThemeBorder?: boolean;
|
||||||
|
$withThemeInherited?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BoxType = ComponentPropsWithRef<typeof Box>;
|
export type BoxType = ComponentPropsWithRef<typeof Box>;
|
||||||
|
|
||||||
export const Box = styled('div')<BoxProps>`
|
export const Box = styled('div')<BoxProps>`
|
||||||
${({ $align }) => $align && `align-items: ${$align};`}
|
${({ $align }) => $align && `align-items: ${$align};`}
|
||||||
${({ $background }) => $background && `background: ${$background};`}
|
|
||||||
${({ $color }) => $color && `color: ${$color};`}
|
|
||||||
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
|
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
|
||||||
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
|
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
|
||||||
${({ $display, as }) =>
|
${({ $display, as }) =>
|
||||||
@@ -57,9 +74,9 @@ export const Box = styled('div')<BoxProps>`
|
|||||||
${({ $height }) => $height && `height: ${$height};`}
|
${({ $height }) => $height && `height: ${$height};`}
|
||||||
${({ $hasTransition }) =>
|
${({ $hasTransition }) =>
|
||||||
$hasTransition && $hasTransition === 'slow'
|
$hasTransition && $hasTransition === 'slow'
|
||||||
? `transition: all 0.5s ease-in-out;`
|
? `transition: all 0.5s var(--c--globals--transitions--ease-out);`
|
||||||
: $hasTransition
|
: $hasTransition
|
||||||
? `transition: all 0.3s ease-in-out;`
|
? `transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);`
|
||||||
: ''}
|
: ''}
|
||||||
${({ $justify }) => $justify && `justify-content: ${$justify};`}
|
${({ $justify }) => $justify && `justify-content: ${$justify};`}
|
||||||
${({ $margin }) => $margin && stylesMargin($margin)}
|
${({ $margin }) => $margin && stylesMargin($margin)}
|
||||||
@@ -73,6 +90,86 @@ export const Box = styled('div')<BoxProps>`
|
|||||||
${({ $position }) => $position && `position: ${$position};`}
|
${({ $position }) => $position && `position: ${$position};`}
|
||||||
${({ $radius }) => $radius && `border-radius: ${$radius};`}
|
${({ $radius }) => $radius && `border-radius: ${$radius};`}
|
||||||
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
|
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
|
||||||
|
${({
|
||||||
|
$layer = 'border',
|
||||||
|
$theme = 'brand',
|
||||||
|
$variation = 'primary',
|
||||||
|
$scope = 'semantic',
|
||||||
|
$border,
|
||||||
|
$withThemeBorder,
|
||||||
|
$withThemeInherited,
|
||||||
|
}) => {
|
||||||
|
if ($border) {
|
||||||
|
return `border: ${$border};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$layer || !$scope || !$theme || !$withThemeBorder) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($withThemeInherited) {
|
||||||
|
return `border: inherit;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `border: 1px solid var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
|
||||||
|
}}
|
||||||
|
${({
|
||||||
|
$layer = 'background',
|
||||||
|
$theme = 'brand',
|
||||||
|
$variation = 'primary',
|
||||||
|
$scope = 'semantic',
|
||||||
|
$background,
|
||||||
|
$withThemeBG,
|
||||||
|
$withThemeInherited,
|
||||||
|
}) => {
|
||||||
|
if ($background) {
|
||||||
|
return `background: ${$background};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$layer || !$scope || !$theme || !$withThemeBG) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($withThemeInherited) {
|
||||||
|
return `background: inherit;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `background: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
|
||||||
|
}}
|
||||||
|
${({
|
||||||
|
$layer = 'content',
|
||||||
|
$theme = 'neutral',
|
||||||
|
$variation = 'primary',
|
||||||
|
$scope = 'semantic',
|
||||||
|
$color,
|
||||||
|
$withThemeBG,
|
||||||
|
$withThemeInherited,
|
||||||
|
}) => {
|
||||||
|
if ($color) {
|
||||||
|
return `color: ${$color};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$layer || !$scope) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a special case when primary with background
|
||||||
|
if (
|
||||||
|
$withThemeBG &&
|
||||||
|
$layer === 'content' &&
|
||||||
|
$scope === 'semantic' &&
|
||||||
|
$variation === 'primary' &&
|
||||||
|
$theme
|
||||||
|
) {
|
||||||
|
$variation = `on-${$theme}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($withThemeInherited) {
|
||||||
|
return `color: inherit;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `color: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
|
||||||
|
}}
|
||||||
${({ $transition }) => $transition && `transition: ${$transition};`}
|
${({ $transition }) => $transition && `transition: ${$transition};`}
|
||||||
${({ $width }) => $width && `width: ${$width};`}
|
${({ $width }) => $width && `width: ${$width};`}
|
||||||
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
|
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
|
||||||
@@ -92,7 +189,7 @@ export const Box = styled('div')<BoxProps>`
|
|||||||
return (
|
return (
|
||||||
effect &&
|
effect &&
|
||||||
`
|
`
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);
|
||||||
${effect}
|
${effect}
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export type BoxButtonType = BoxType & {
|
|||||||
*/
|
*/
|
||||||
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||||
({ $css, ...props }, ref) => {
|
({ $css, ...props }, ref) => {
|
||||||
|
const theme = props.$theme || 'gray';
|
||||||
|
const variation = props.$variation || 'primary';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -37,10 +40,15 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
|||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
color: ${props.disabled &&
|
||||||
color: ${props.disabled
|
`var(--c--contextuals--content--semantic--disabled--primary)`};
|
||||||
? 'var(--c--theme--colors--greyscale-400) !important'
|
&:focus-visible {
|
||||||
: 'inherit'};
|
transition: none;
|
||||||
|
outline: 2px solid
|
||||||
|
var(--c--contextuals--content--semantic--${theme}--${variation});
|
||||||
|
border-radius: 1px;
|
||||||
|
outline-offset: var(--c--globals--spacings--st);
|
||||||
|
}
|
||||||
${$css || ''}
|
${$css || ''}
|
||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { css } from 'styled-components';
|
|
||||||
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
|
|
||||||
import { Box, BoxType } from '.';
|
import { Box, BoxType } from '.';
|
||||||
|
|
||||||
export const Card = ({
|
export const Card = ({
|
||||||
children,
|
children,
|
||||||
$css,
|
className,
|
||||||
...props
|
...props
|
||||||
}: PropsWithChildren<BoxType>) => {
|
}: PropsWithChildren<BoxType>) => {
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={`--docs--card ${props.className || ''}`}
|
role="region"
|
||||||
$background="white"
|
$withThemeBG
|
||||||
$radius="4px"
|
$withThemeBorder
|
||||||
$css={css`
|
className={`--docs--card ${className || ''}`}
|
||||||
border: 1px solid ${colorsTokens['greyscale-200']};
|
$radius="var(--c--globals--spacings--st)"
|
||||||
${$css}
|
$padding={{ horizontal: 'xs', vertical: '3xs' }}
|
||||||
`}
|
$scope={props.$theme ? props.$scope || 'semantic' : 'surface'}
|
||||||
|
$theme={props.$theme || 'primary'}
|
||||||
|
$variation={props.$theme ? props.$variation || 'tertiary' : ''}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import { BoxProps } from './Box';
|
|||||||
|
|
||||||
const StyledPopover = styled(Popover)`
|
const StyledPopover = styled(Popover)`
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 4px;
|
border-radius: var(--c--globals--spacings--st);
|
||||||
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 6px 0 rgba(0, 0, 145, 0.1);
|
||||||
border: 1px solid #dddddd;
|
border: 1px solid var(--c--contextuals--border--surface--primary);
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity var(--c--globals--transitions--duration)
|
||||||
|
var(--c--globals--transitions--ease-out);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface StyledButtonProps {
|
interface StyledButtonProps {
|
||||||
@@ -28,19 +29,24 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
|||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-weight: 500;
|
font-weight: var(--c--components--button--font-weight);
|
||||||
font-size: 0.938rem;
|
font-size: var(--c--components--button--medium-font-size);
|
||||||
padding: 0;
|
padding: var(--c--globals--spacings--0);
|
||||||
${({ $css }) => $css};
|
border-radius: var(--c--globals--spacings--st);
|
||||||
|
color: var(--c--contextuals--content--semantic--brand--tertiary);
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--c--components--button--primary-text--background--color-hover
|
--c--contextuals--background--semantic--contextual--primary
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
|
box-shadow: 0 0 0 2px var(--c--globals--colors--brand-400);
|
||||||
border-radius: 4px;
|
background-color: var(
|
||||||
|
--c--contextuals--background--semantic--brand--tertiary-hover
|
||||||
|
);
|
||||||
|
border-radius: var(--c--globals--spacings--st);
|
||||||
}
|
}
|
||||||
|
${({ $css }) => $css};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface DropButtonProps {
|
export interface DropButtonProps {
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import { css } from 'styled-components';
|
|||||||
import { Text, TextType } from '@/components';
|
import { Text, TextType } from '@/components';
|
||||||
|
|
||||||
type IconProps = TextType & {
|
type IconProps = TextType & {
|
||||||
|
disabled?: boolean;
|
||||||
iconName: string;
|
iconName: string;
|
||||||
variant?: 'filled' | 'outlined';
|
variant?: 'filled' | 'outlined' | 'symbols-outlined';
|
||||||
};
|
};
|
||||||
export const Icon = ({
|
export const Icon = ({
|
||||||
|
className,
|
||||||
iconName,
|
iconName,
|
||||||
|
disabled,
|
||||||
variant = 'outlined',
|
variant = 'outlined',
|
||||||
|
$theme = 'neutral',
|
||||||
...textProps
|
...textProps
|
||||||
}: IconProps) => {
|
}: IconProps) => {
|
||||||
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
||||||
@@ -18,12 +22,15 @@ export const Icon = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
{...textProps}
|
|
||||||
aria-hidden={ariaHidden}
|
aria-hidden={ariaHidden}
|
||||||
className={clsx('--docs--icon-bg', textProps.className, {
|
className={clsx('--docs--icon-bg', className, {
|
||||||
'material-icons-filled': variant === 'filled',
|
'material-icons-filled': variant === 'filled',
|
||||||
'material-icons': variant === 'outlined',
|
'material-icons': variant === 'outlined',
|
||||||
|
'material-symbols-outlined': variant === 'symbols-outlined',
|
||||||
})}
|
})}
|
||||||
|
$theme={disabled ? 'disabled' : $theme}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
{...textProps}
|
||||||
>
|
>
|
||||||
{iconName}
|
{iconName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -34,15 +41,19 @@ type IconOptionsProps = TextType & {
|
|||||||
isHorizontal?: boolean;
|
isHorizontal?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
|
export const IconOptions = ({
|
||||||
|
isHorizontal,
|
||||||
|
$css,
|
||||||
|
...props
|
||||||
|
}: IconOptionsProps) => {
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
{...props}
|
|
||||||
iconName={isHorizontal ? 'more_horiz' : 'more_vert'}
|
iconName={isHorizontal ? 'more_horiz' : 'more_vert'}
|
||||||
$css={css`
|
$css={css`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
${props.$css}
|
${$css}
|
||||||
`}
|
`}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ export const InfiniteScroll = ({
|
|||||||
{!isLoading && hasMore && (
|
{!isLoading && hasMore && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void next()}
|
onClick={() => void next()}
|
||||||
color="primary-text"
|
color="brand"
|
||||||
|
variant="bordered"
|
||||||
icon={<Icon iconName="arrow_downward" />}
|
icon={<Icon iconName="arrow_downward" />}
|
||||||
>
|
>
|
||||||
{buttonLabel ?? t('Load more')}
|
{buttonLabel ?? t('Load more')}
|
||||||
|
|||||||