Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1b41cc4c4 | |||
| 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 |
@@ -59,6 +59,14 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Checkout custom code repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'AntoLC/docs-customized'
|
||||||
|
ref: 'main'
|
||||||
|
path: docs-custom
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -76,7 +84,7 @@ jobs:
|
|||||||
name: Run trivy scan
|
name: Run trivy scan
|
||||||
uses: numerique-gouv/action-trivy-cache@main
|
uses: numerique-gouv/action-trivy-cache@main
|
||||||
with:
|
with:
|
||||||
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production --build-arg CUSTOM_CODE=docs-custom'
|
||||||
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
|
docker-image-name: 'docker.io/lasuite/impress-frontend:${{ github.sha }}'
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
@@ -87,6 +95,7 @@ jobs:
|
|||||||
target: frontend-production
|
target: frontend-production
|
||||||
build-args: |
|
build-args: |
|
||||||
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||||
|
CUSTOM_CODE=docs-custom
|
||||||
PUBLISH_AS_MIT=false
|
PUBLISH_AS_MIT=false
|
||||||
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
|
push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+57
-4
@@ -6,23 +6,73 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- ♿(frontend) improve accessibility:
|
||||||
|
- ♿(frontend) improve share modal button accessibility #1626
|
||||||
|
|
||||||
|
## [3.10.0] - 2025-11-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- ✨(export) enable ODT export for documents #1524
|
||||||
|
- ✨(frontend) improve mobile UX by showing subdocs count #1540
|
||||||
|
|
||||||
|
### 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
|
||||||
|
- 🐛(frontend) preserve left panel width on window resize #1588
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- 🔥(backend) remove api managing templates
|
||||||
|
|
||||||
|
|
||||||
|
## [3.9.0] - 2025-11-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(frontend) create skeleton component for DocEditor #1491
|
- ✨(frontend) create skeleton component for DocEditor #1491
|
||||||
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
|
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
|
||||||
|
- ✨(frontend) ajustable left panel #1456
|
||||||
|
- ✨ Add comments feature to the editor #1330
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- ♻️(frontend) adapt custom blocks to new implementation #1375
|
- ♻️(frontend) adapt custom blocks to new implementation #1375
|
||||||
- ♻️(backend) increase user short_name field length
|
- ♻️(backend) increase user short_name field length #1510
|
||||||
|
- 🚸(frontend) separate viewers from editors #1509
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- 🐛(frontend) fix duplicate document entries in grid #1479
|
- 🐛(frontend) fix duplicate document entries in grid #1479
|
||||||
- 🐛(frontend) show full nested doc names with ajustable bar #1456
|
- 🐛(backend) fix trashbin list #1520
|
||||||
- 🐛(backend) fix trashbin list
|
|
||||||
- ♿(frontend) improve accessibility:
|
- ♿(frontend) improve accessibility:
|
||||||
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
|
- ♿(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
|
## [3.8.2] - 2025-10-17
|
||||||
|
|
||||||
@@ -133,6 +183,7 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- ✨(backend) Comments on text editor #1309
|
||||||
- 👷(CI) add bundle size check job #1268
|
- 👷(CI) add bundle size check job #1268
|
||||||
- ✨(frontend) use title first emoji as doc icon in tree #1289
|
- ✨(frontend) use title first emoji as doc icon in tree #1289
|
||||||
|
|
||||||
@@ -835,7 +886,9 @@ 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.8.2...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.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
|
||||||
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
|
[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.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
+1
-41
@@ -85,44 +85,4 @@ THEME_CUSTOMIZATION_FILE_PATH=<path>
|
|||||||
|
|
||||||
### Example of JSON
|
### Example of JSON
|
||||||
|
|
||||||
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
The json must follow some rules: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
# **Custom Export Templates** 📄
|
|
||||||
|
|
||||||
You can define custom export templates to add introductory content, such as headers or titles, to documents before exporting them as PDF, Docx, etc...
|
|
||||||
|
|
||||||
Export Templates are managed through the admin interface and can be selected by users during the export process.
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
|
|
||||||
This feature offers several advantages:
|
|
||||||
* **Header customization** 📄: Add custom headers, titles, or branding to exported documents.
|
|
||||||
* **No code changes required** 🔧: Templates are managed through the admin interface without needing developer intervention.
|
|
||||||
* **Flexible content** 🌟: Use HTML to create headers that match your organization's style.
|
|
||||||
|
|
||||||
### Limitations ⚠️
|
|
||||||
|
|
||||||
- Currently, templates are only prepended to the document.
|
|
||||||
More complex layouts are not supported at this time.
|
|
||||||
- The `CSS` and `Description` fields are being ignored at this time.
|
|
||||||
- <b>Due to technical conversion limitations, not all HTML can be converted to the internal format!</b>
|
|
||||||
|
|
||||||
### How to Use
|
|
||||||
|
|
||||||
1. Create the Template in a new document.
|
|
||||||

|
|
||||||
2. Copy it as HTML code using the `Copy as HTML` feature:
|
|
||||||

|
|
||||||
3. Log in to the admin interface at `/admin` (backend container).
|
|
||||||
2. Create a new template:
|
|
||||||

|
|
||||||
- **Title**: Enter a descriptive name for the template.
|
|
||||||
- **Code**: Paste the HTML content from step 2.
|
|
||||||
- **Public**: Check this box to make the template available in the frontend export modal.
|
|
||||||
- **Save** the template.
|
|
||||||
|
|
||||||
Once saved, users can select the template from the export modal in the frontend during the export process.
|
|
||||||

|
|
||||||
|
|
||||||
@@ -19,6 +19,12 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
@@ -786,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,
|
||||||
)
|
)
|
||||||
@@ -810,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 {}
|
||||||
|
|
||||||
|
|
||||||
@@ -889,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 _
|
||||||
@@ -1105,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):
|
||||||
@@ -1831,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"""
|
||||||
@@ -1890,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,
|
||||||
@@ -2162,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -2236,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)
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
+163
-17
@@ -756,6 +756,7 @@ class Document(MP_Node, BaseModel):
|
|||||||
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
|
||||||
@@ -786,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,
|
||||||
@@ -1146,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)
|
||||||
@@ -1205,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,
|
||||||
@@ -1229,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):
|
||||||
@@ -1278,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"],
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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,
|
||||||
@@ -113,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,
|
||||||
@@ -220,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,
|
||||||
@@ -229,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,
|
||||||
@@ -304,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,
|
||||||
@@ -494,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,
|
||||||
@@ -516,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,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,7 @@ def test_api_documents_trashbin_format():
|
|||||||
"collaboration_auth": False,
|
"collaboration_auth": False,
|
||||||
"descendants": False,
|
"descendants": False,
|
||||||
"cors_proxy": False,
|
"cors_proxy": False,
|
||||||
|
"comment": False,
|
||||||
"content": False,
|
"content": False,
|
||||||
"destroy": False,
|
"destroy": False,
|
||||||
"duplicate": False,
|
"duplicate": False,
|
||||||
@@ -88,8 +89,8 @@ def test_api_documents_trashbin_format():
|
|||||||
"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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -385,6 +462,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
|||||||
"children_create": False,
|
"children_create": False,
|
||||||
"children_list": False,
|
"children_list": False,
|
||||||
"collaboration_auth": False,
|
"collaboration_auth": False,
|
||||||
|
"comment": False,
|
||||||
"descendants": False,
|
"descendants": False,
|
||||||
"cors_proxy": False,
|
"cors_proxy": False,
|
||||||
"content": False,
|
"content": False,
|
||||||
@@ -394,8 +472,8 @@ def test_models_documents_get_abilities_owner(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": False,
|
"mask": False,
|
||||||
@@ -430,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,
|
||||||
@@ -439,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,
|
||||||
@@ -485,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,
|
||||||
@@ -494,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,
|
||||||
@@ -547,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,
|
||||||
@@ -556,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,
|
||||||
@@ -607,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,
|
||||||
@@ -616,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,
|
||||||
@@ -1320,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"]}),
|
||||||
@@ -1328,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"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -1342,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"],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -1360,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,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1376,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,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+12
-10
@@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Doare korf"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Stumm"
|
msgstr "Stumm"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Restr hep titl"
|
msgstr "Restr hep titl"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: 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:996 core/models.py:996
|
#: 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:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "deskrivadur"
|
msgstr "deskrivadur"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "kod"
|
msgstr "kod"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "publik"
|
msgstr "publik"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Patrom"
|
msgstr "Patrom"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Patromoù"
|
msgstr "Patromoù"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Typ"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Format"
|
msgstr "Format"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Unbenanntes Dokument"
|
msgstr "Unbenanntes Dokument"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: 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:996 core/models.py:996
|
#: 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:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "Beschreibung"
|
msgstr "Beschreibung"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "Code"
|
msgstr "Code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "CSS"
|
msgstr "CSS"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "öffentlich"
|
msgstr "öffentlich"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Vorlage"
|
msgstr "Vorlage"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Vorlagen"
|
msgstr "Vorlagen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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 ""
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr ""
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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 ""
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:996 core/models.py:996
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
#: 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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Tipo de Cuerpo"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formato"
|
msgstr "Formato"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: 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:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: 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:996 core/models.py:996
|
#: 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:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: 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:1284 core/models.py:1284
|
#: 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:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: 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:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Plantilla"
|
msgstr "Plantilla"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Plantillas"
|
msgstr "Plantillas"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Type de corps"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Format"
|
msgstr "Format"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Document sans titre"
|
msgstr "Document sans titre"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: 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:996 core/models.py:996
|
#: 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:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "description"
|
msgstr "description"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "code"
|
msgstr "code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "CSS"
|
msgstr "CSS"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "public"
|
msgstr "public"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: 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:1296 core/models.py:1296
|
#: 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:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr ""
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formato"
|
msgstr "Formato"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Documento senza titolo"
|
msgstr "Documento senza titolo"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: 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:996 core/models.py:996
|
#: 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:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "descrizione"
|
msgstr "descrizione"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "code"
|
msgstr "code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "pubblico"
|
msgstr "pubblico"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Modello"
|
msgstr "Modello"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Modelli"
|
msgstr "Modelli"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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,7 +40,7 @@ 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"
|
||||||
@@ -48,15 +48,15 @@ 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:496 core/api/serializers.py:496
|
#: 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:500 core/api/serializers.py:500
|
#: 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:536 core/api/serializers.py:536
|
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
|
||||||
msgid "This field is required."
|
msgid "This field is required."
|
||||||
@@ -79,7 +79,7 @@ msgstr "Text type"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formaat"
|
msgstr "Formaat"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -92,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"
|
||||||
@@ -104,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"
|
||||||
@@ -148,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"
|
||||||
@@ -164,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"
|
||||||
@@ -172,234 +172,234 @@ 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 "Vereist. Minder dan 255 ASCII tekens."
|
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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Naamloos Document"
|
msgstr "Naamloos Document"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: 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:996 core/models.py:996
|
#: 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:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: build/lib/core/models.py:1191 core/models.py:1191
|
||||||
msgid "Document ask for access"
|
msgid "Document ask for access"
|
||||||
msgstr "Document verzoekt om toegang"
|
msgstr "Document verzoekt om toegang"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
#: build/lib/core/models.py:1192 core/models.py:1192
|
||||||
msgid "Document ask for accesses"
|
msgid "Document ask for accesses"
|
||||||
msgstr "Document verzoekt om toegangen"
|
msgstr "Document verzoekt om toegangen"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
#: 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 "Deze gebruiker heeft al om toegang tot dit document gevraagd."
|
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
#: 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} wenst toegang tot een document!"
|
msgstr "{name} verzoekt toegang tot een document!"
|
||||||
|
|
||||||
|
#: build/lib/core/models.py:1259 core/models.py:1259
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{name} would like access to the following document:"
|
||||||
|
msgstr "{name} verzoekt toegang tot het volgende document:"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{name} would like access to the following document:"
|
|
||||||
msgstr "{name} wenst toegang tot het volgende document:"
|
|
||||||
|
|
||||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
|
||||||
#, 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} vraagt toegang tot het document: {title}"
|
msgstr "{name} verzoekt toegang tot het document: {title}"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "omschrijving"
|
msgstr "omschrijving"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "code"
|
msgstr "code"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "publiek"
|
msgstr "publiek"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Template"
|
msgstr "Sjabloon"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Templates"
|
msgstr "Sjabloon"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1344 core/models.py:1344
|
||||||
|
msgid "Template/user relations"
|
||||||
|
msgstr "Sjabloon/gebruiker relaties"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||||
msgid "Template/user relations"
|
msgid "This user is already in this template."
|
||||||
msgstr "Template/gebruiker relaties"
|
msgstr "De gebruiker bestaat al in dit sjabloon."
|
||||||
|
|
||||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||||
msgid "This user is already in this template."
|
|
||||||
msgstr "De gebruiker bestaat al in dit template."
|
|
||||||
|
|
||||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
|
||||||
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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Tipo de corpo"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Formato"
|
msgstr "Formato"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:996 core/models.py:996
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
#: 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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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 "Древовидная структура"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Тип сообщения"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Формат"
|
msgstr "Формат"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Безымянный документ"
|
msgstr "Безымянный документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Избранный документ"
|
msgstr "Избранный документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:996 core/models.py:996
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Избранные документы"
|
msgstr "Избранные документы"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "описание"
|
msgstr "описание"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "код"
|
msgstr "код"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "доступно всем"
|
msgstr "доступно всем"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Шаблон"
|
msgstr "Шаблон"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Шаблоны"
|
msgstr "Шаблоны"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "адрес электронной почты"
|
msgstr "адрес электронной почты"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Приглашение для документа"
|
msgstr "Приглашение для документа"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Приглашения для документов"
|
msgstr "Приглашения для документов"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
#: 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 "Этот адрес уже связан с зарегистрированным пользователем."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Vrsta telesa"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Oblika"
|
msgstr "Oblika"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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 ""
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Dokument brez naslova"
|
msgstr "Dokument brez naslova"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: 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:996 core/models.py:996
|
#: 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:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "opis"
|
msgstr "opis"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "koda"
|
msgstr "koda"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "javno"
|
msgstr "javno"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Predloga"
|
msgstr "Predloga"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Predloge"
|
msgstr "Predloge"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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 ""
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr ""
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Format"
|
msgstr "Format"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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 ""
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:996 core/models.py:996
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: 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:1458 core/models.py:1458
|
#: 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:1459 core/models.py:1459
|
#: 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:1479 core/models.py:1479
|
#: 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."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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 ""
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr ""
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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 ""
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:996 core/models.py:996
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
#: 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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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 "Ієрархічна структура"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "Тип вмісту"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "Формат"
|
msgstr "Формат"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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}"
|
||||||
@@ -178,228 +178,228 @@ 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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "Документ без назви"
|
msgstr "Документ без назви"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "Обраний документ"
|
msgstr "Обраний документ"
|
||||||
|
|
||||||
#: build/lib/core/models.py:996 core/models.py:996
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "Обрані документи"
|
msgstr "Обрані документи"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "опис"
|
msgstr "опис"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "код"
|
msgstr "код"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "публічне"
|
msgstr "публічне"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Шаблон"
|
msgstr "Шаблон"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "Шаблони"
|
msgstr "Шаблони"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "електронна адреса"
|
msgstr "електронна адреса"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "Запрошення до редагування документа"
|
msgstr "Запрошення до редагування документа"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "Запрошення до редагування документів"
|
msgstr "Запрошення до редагування документів"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
#: 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 "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
||||||
|
|
||||||
|
|||||||
@@ -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-10-14 07:19+0000\n"
|
"POT-Creation-Date: 2025-11-17 08:04+0000\n"
|
||||||
"PO-Revision-Date: 2025-10-14 13:09\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 "树状结构"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ msgstr "正文类型"
|
|||||||
msgid "Format"
|
msgid "Format"
|
||||||
msgstr "格式"
|
msgstr "格式"
|
||||||
|
|
||||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
#: 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} 的副本"
|
||||||
@@ -178,228 +178,228 @@ msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
|
|||||||
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:1282
|
#: build/lib/core/models.py:361 build/lib/core/models.py:1276
|
||||||
#: core/models.py:359 core/models.py:1282
|
#: 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:820 core/models.py:422
|
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
|
||||||
#: core/models.py:820
|
#: core/models.py:822
|
||||||
msgid "Untitled Document"
|
msgid "Untitled Document"
|
||||||
msgstr "未命名文档"
|
msgstr "未命名文档"
|
||||||
|
|
||||||
#: build/lib/core/models.py:855 core/models.py:855
|
#: 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:859 core/models.py:859
|
#: 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:865 core/models.py:865
|
#: 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:965 core/models.py:965
|
#: 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:966 core/models.py:966
|
#: 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:972 core/models.py:972
|
#: 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:995 core/models.py:995
|
#: build/lib/core/models.py:997 core/models.py:997
|
||||||
msgid "Document favorite"
|
msgid "Document favorite"
|
||||||
msgstr "文档收藏"
|
msgstr "文档收藏"
|
||||||
|
|
||||||
#: build/lib/core/models.py:996 core/models.py:996
|
#: build/lib/core/models.py:998 core/models.py:998
|
||||||
msgid "Document favorites"
|
msgid "Document favorites"
|
||||||
msgstr "文档收藏夹"
|
msgstr "文档收藏夹"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
#: 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:1024 core/models.py:1024
|
#: 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:1025 core/models.py:1025
|
#: 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:1031 core/models.py:1031
|
#: 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:1037 core/models.py:1037
|
#: 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:1043 build/lib/core/models.py:1368
|
#: build/lib/core/models.py:1045 build/lib/core/models.py:1362
|
||||||
#: core/models.py:1043 core/models.py:1368
|
#: 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:1189 core/models.py:1189
|
#: 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:1190 core/models.py:1190
|
#: 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:1196 core/models.py:1196
|
#: 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:1261 core/models.py:1261
|
#: 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:1265 core/models.py:1265
|
#: 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:1271 core/models.py:1271
|
#: 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:1283 core/models.py:1283
|
#: build/lib/core/models.py:1277 core/models.py:1277
|
||||||
msgid "description"
|
msgid "description"
|
||||||
msgstr "说明"
|
msgstr "说明"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
#: build/lib/core/models.py:1278 core/models.py:1278
|
||||||
msgid "code"
|
msgid "code"
|
||||||
msgstr "代码"
|
msgstr "代码"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
#: build/lib/core/models.py:1279 core/models.py:1279
|
||||||
msgid "css"
|
msgid "css"
|
||||||
msgstr "css"
|
msgstr "css"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
#: build/lib/core/models.py:1281 core/models.py:1281
|
||||||
msgid "public"
|
msgid "public"
|
||||||
msgstr "公开"
|
msgstr "公开"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
#: 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:1295 core/models.py:1295
|
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "模板"
|
msgstr "模板"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
#: build/lib/core/models.py:1290 core/models.py:1290
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr "模板"
|
msgstr "模板"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
#: 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:1350 core/models.py:1350
|
#: 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:1356 core/models.py:1356
|
#: 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:1362 core/models.py:1362
|
#: 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:1439 core/models.py:1439
|
#: build/lib/core/models.py:1433 core/models.py:1433
|
||||||
msgid "email address"
|
msgid "email address"
|
||||||
msgstr "电子邮件地址"
|
msgstr "电子邮件地址"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
#: build/lib/core/models.py:1452 core/models.py:1452
|
||||||
msgid "Document invitation"
|
msgid "Document invitation"
|
||||||
msgstr "文档邀请"
|
msgstr "文档邀请"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
#: build/lib/core/models.py:1453 core/models.py:1453
|
||||||
msgid "Document invitations"
|
msgid "Document invitations"
|
||||||
msgstr "文档邀请"
|
msgstr "文档邀请"
|
||||||
|
|
||||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
#: 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 "此电子邮件已经与现有注册用户关联。"
|
||||||
|
|
||||||
|
|||||||
+32
-32
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "impress"
|
name = "impress"
|
||||||
version = "3.8.2"
|
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]
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ ENV NEXT_PUBLIC_SW_DEACTIVATED=${SW_DEACTIVATED}
|
|||||||
ARG PUBLISH_AS_MIT
|
ARG PUBLISH_AS_MIT
|
||||||
ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
|
ENV NEXT_PUBLIC_PUBLISH_AS_MIT=${PUBLISH_AS_MIT}
|
||||||
|
|
||||||
|
ARG CUSTOM_CODE
|
||||||
|
COPY ./${CUSTOM_CODE} /tmp/custom_code
|
||||||
|
RUN if [ -n "$CUSTOM_CODE" ] && [ -d "/tmp/custom_code" ] && [ "$(ls -A /tmp/custom_code)" ]; then \
|
||||||
|
echo "Custom code provided. Replacing files from $CUSTOM_CODE..."; \
|
||||||
|
cp -Rv /tmp/custom_code/${CUSTOM_CODE}/* .; \
|
||||||
|
else \
|
||||||
|
echo "No custom code provided. Skipping replacement..."; \
|
||||||
|
fi
|
||||||
|
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# ---- Front-end image ----
|
# ---- Front-end image ----
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -494,7 +494,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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -518,6 +518,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,
|
||||||
@@ -800,6 +806,12 @@ test.describe('Doc Editor', () => {
|
|||||||
});
|
});
|
||||||
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
|
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
|
||||||
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
|
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('@');
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await expect(editor.getByText('@')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks multiple big doc scroll to the top', async ({
|
test('it checks multiple big doc scroll to the top', async ({
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ 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';
|
||||||
@@ -32,7 +31,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' }),
|
||||||
@@ -86,11 +85,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.total).toBe(2);
|
expect(pdfInfo.total).toBe(2);
|
||||||
expect(pdfData.text).toContain('Hello\n\nWorld\n\n'); // 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 }) => {
|
||||||
@@ -138,6 +142,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
|
||||||
@@ -219,10 +268,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 }) => {
|
||||||
@@ -265,9 +314,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');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it exports the doc with multi columns', async ({
|
test('it exports the doc with multi columns', async ({
|
||||||
@@ -320,46 +369,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', {
|
||||||
@@ -447,8 +483,72 @@ 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`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,6 +134,8 @@ 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();
|
||||||
@@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,18 @@ test.describe('Doc Header', () => {
|
|||||||
await createDoc(page, 'doc-update-emoji', browserName, 1);
|
await createDoc(page, 'doc-update-emoji', browserName, 1);
|
||||||
|
|
||||||
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
|
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
|
// Top parent should not have emoji picker
|
||||||
await expect(emojiPicker).toBeHidden();
|
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(
|
const { name: docChild } = await createRootSubPage(
|
||||||
page,
|
page,
|
||||||
@@ -80,13 +89,23 @@ test.describe('Doc Header', () => {
|
|||||||
|
|
||||||
await verifyDocName(page, docChild);
|
await verifyDocName(page, docChild);
|
||||||
|
|
||||||
await expect(emojiPicker).toBeVisible();
|
// 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({
|
await emojiPicker.click({
|
||||||
delay: 100,
|
delay: 100,
|
||||||
});
|
});
|
||||||
await page.getByRole('button', { name: '😀' }).first().click();
|
await page.getByRole('button', { name: '😀' }).first().click();
|
||||||
await expect(emojiPicker).toHaveText('😀');
|
await expect(emojiPicker).toHaveText('😀');
|
||||||
|
|
||||||
|
// Update title
|
||||||
const docTitle = page.getByRole('textbox', { name: 'Document title' });
|
const docTitle = page.getByRole('textbox', { name: 'Document title' });
|
||||||
await docTitle.fill('Hello Emoji World');
|
await docTitle.fill('Hello Emoji World');
|
||||||
await docTitle.blur();
|
await docTitle.blur();
|
||||||
@@ -95,6 +114,12 @@ test.describe('Doc Header', () => {
|
|||||||
// Check the tree
|
// Check the tree
|
||||||
const row = await getTreeRow(page, 'Hello Emoji World');
|
const row = await getTreeRow(page, 'Hello Emoji World');
|
||||||
await expect(row.getByText('😀')).toBeVisible();
|
await expect(row.getByText('😀')).toBeVisible();
|
||||||
|
|
||||||
|
// Remove emoji
|
||||||
|
await optionMenu.click();
|
||||||
|
await expect(addEmojiMenuItem).toBeHidden();
|
||||||
|
await removeEmojiMenuItem.click();
|
||||||
|
await expect(emojiPicker).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it deletes the doc', async ({ page, browserName }) => {
|
test('it deletes the doc', async ({ page, browserName }) => {
|
||||||
|
|||||||
@@ -84,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.getByRole('button', { name: /^Invite / }).click();
|
||||||
|
|
||||||
// Check invitation added
|
// Check invitation added
|
||||||
await expect(
|
await expect(
|
||||||
@@ -135,7 +135,7 @@ test.describe('Document create member', () => {
|
|||||||
(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.getByRole('button', { name: /^Invite / }).click();
|
||||||
|
|
||||||
// Check invitation sent
|
// Check invitation sent
|
||||||
|
|
||||||
@@ -154,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.getByRole('button', { name: /^Invite / }).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();
|
||||||
@@ -191,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.getByRole('button', { name: /^Invite / }).click();
|
||||||
|
|
||||||
// Check invitation sent
|
// Check invitation sent
|
||||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||||
|
|||||||
@@ -31,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';
|
||||||
@@ -160,6 +160,9 @@ 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
|
||||||
@@ -186,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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ test.describe('Doc Trashbin', () => {
|
|||||||
await row.getByText(subDocName).click();
|
await row.getByText(subDocName).click();
|
||||||
await verifyDocName(page, subDocName);
|
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.getByLabel('Alert deleted document')).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
|
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
|
||||||
await expect(page.locator('.bn-editor')).toHaveAttribute(
|
await expect(page.locator('.bn-editor')).toHaveAttribute(
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ test.describe('Doc Tree', () => {
|
|||||||
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
|
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
|
||||||
|
|
||||||
await expect(row.getByText('😀')).toBeHidden();
|
await expect(row.getByText('😀')).toBeHidden();
|
||||||
await expect(titleEmojiPicker).not.toHaveText('😀');
|
await expect(titleEmojiPicker).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
keyCloakSignIn,
|
keyCloakSignIn,
|
||||||
verifyDocName,
|
verifyDocName,
|
||||||
} from './utils-common';
|
} from './utils-common';
|
||||||
import { writeInEditor } from './utils-editor';
|
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';
|
||||||
|
|
||||||
@@ -182,15 +182,14 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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',
|
||||||
@@ -200,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();
|
||||||
@@ -241,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();
|
||||||
@@ -317,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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Type definitions for pdf-parse library
|
|
||||||
* The library doesn't export complete type definitions for the parsed PDF data
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare module 'pdf-parse' {
|
|
||||||
export interface PdfInfo {
|
|
||||||
Title?: string;
|
|
||||||
Author?: string;
|
|
||||||
Subject?: string;
|
|
||||||
Keywords?: string;
|
|
||||||
Creator?: string;
|
|
||||||
Producer?: string;
|
|
||||||
CreationDate?: string;
|
|
||||||
ModDate?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PdfData {
|
|
||||||
/** Total number of pages */
|
|
||||||
numpages: number;
|
|
||||||
/** Alias for numpages */
|
|
||||||
total?: number;
|
|
||||||
/** Extracted text content from the PDF */
|
|
||||||
text: string;
|
|
||||||
/** PDF metadata information */
|
|
||||||
info?: PdfInfo;
|
|
||||||
/** PDF metadata (alternative structure) */
|
|
||||||
metadata?: unknown;
|
|
||||||
/** PDF version */
|
|
||||||
version?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pdf(buffer: Buffer): Promise<PdfData>;
|
|
||||||
|
|
||||||
export default pdf;
|
|
||||||
}
|
|
||||||
@@ -31,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({
|
||||||
@@ -70,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}`;
|
||||||
@@ -125,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();
|
||||||
}
|
}
|
||||||
@@ -204,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') &&
|
||||||
@@ -256,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({
|
||||||
@@ -277,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: {
|
||||||
@@ -300,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: {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
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 = 'user.test',
|
fillText = 'user.test',
|
||||||
) => {
|
) => {
|
||||||
const responsePromiseSearchUser = page.waitForResponse(
|
const responsePromiseSearchUser = page.waitForResponse(
|
||||||
@@ -40,7 +40,7 @@ export const addNewMember = async (
|
|||||||
// Choose a role
|
// Choose a role
|
||||||
await page.getByLabel('doc-role-dropdown').click();
|
await page.getByLabel('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.getByRole('button', { name: /^Invite / }).click();
|
||||||
|
|
||||||
return users[index].email;
|
return users[index].email;
|
||||||
};
|
};
|
||||||
@@ -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,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-e2e",
|
"name": "app-e2e",
|
||||||
"version": "3.8.2",
|
"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.1",
|
"@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": "2.1.7"
|
"pdf-parse": "2.4.5"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22"
|
"packageManager": "yarn@1.22.22"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ const dsfrTheme = {
|
|||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
families: {
|
families: {
|
||||||
base: 'Marianne',
|
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||||
accent: 'Marianne',
|
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-impress",
|
"name": "app-impress",
|
||||||
"version": "3.8.2",
|
"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,50 +19,51 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ag-media/react-pdf-table": "2.0.3",
|
"@ag-media/react-pdf-table": "2.0.3",
|
||||||
"@blocknote/code-block": "0.41.1",
|
"@blocknote/code-block": "0.42.3",
|
||||||
"@blocknote/core": "0.41.1",
|
"@blocknote/core": "0.42.3",
|
||||||
"@blocknote/mantine": "0.41.1",
|
"@blocknote/mantine": "0.42.3",
|
||||||
"@blocknote/react": "0.41.1",
|
"@blocknote/react": "0.42.3",
|
||||||
"@blocknote/xl-docx-exporter": "0.41.1",
|
"@blocknote/xl-docx-exporter": "0.42.3",
|
||||||
"@blocknote/xl-multi-column": "0.41.1",
|
"@blocknote/xl-multi-column": "0.42.3",
|
||||||
"@blocknote/xl-pdf-exporter": "0.41.1",
|
"@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-variable/material-symbols-outlined": "5.2.25",
|
"@fontsource-variable/material-symbols-outlined": "5.2.28",
|
||||||
"@fontsource/material-icons": "5.2.7",
|
"@fontsource/material-icons": "5.2.7",
|
||||||
"@gouvfr-lasuite/integration": "1.0.3",
|
"@gouvfr-lasuite/integration": "1.0.3",
|
||||||
"@gouvfr-lasuite/ui-kit": "0.16.2",
|
"@gouvfr-lasuite/ui-kit": "0.16.2",
|
||||||
"@hocuspocus/provider": "3.3.0",
|
"@hocuspocus/provider": "3.4.0",
|
||||||
"@mantine/core": "8.3.4",
|
"@mantine/core": "8.3.6",
|
||||||
"@mantine/hooks": "8.3.4",
|
"@mantine/hooks": "8.3.6",
|
||||||
"@openfun/cunningham-react": "3.2.3",
|
"@openfun/cunningham-react": "3.2.3",
|
||||||
"@react-pdf/renderer": "4.3.1",
|
"@react-pdf/renderer": "4.3.1",
|
||||||
"@sentry/nextjs": "10.17.0",
|
"@sentry/nextjs": "10.22.0",
|
||||||
"@tanstack/react-query": "5.90.2",
|
"@tanstack/react-query": "5.90.6",
|
||||||
"@tiptap/extensions": "3.4.4",
|
"@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": "*",
|
"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.3",
|
"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.4",
|
"next": "15.5.4",
|
||||||
"posthog-js": "1.271.0",
|
"posthog-js": "1.284.0",
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-aria-components": "1.13.0",
|
"react-aria-components": "1.13.0",
|
||||||
"react-dom": "*",
|
"react-dom": "*",
|
||||||
"react-i18next": "16.0.0",
|
"react-i18next": "16.3.3",
|
||||||
"react-intersection-observer": "9.16.0",
|
"react-intersection-observer": "10.0.0",
|
||||||
"react-resizable-panels": "3.0.6",
|
"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",
|
||||||
@@ -83,13 +84,13 @@
|
|||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"@types/react-dom": "*",
|
"@types/react-dom": "*",
|
||||||
"@vitejs/plugin-react": "5.0.4",
|
"@vitejs/plugin-react": "5.1.0",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"cross-env": "10.1.0",
|
"cross-env": "10.1.0",
|
||||||
"dotenv": "17.2.3",
|
"dotenv": "17.2.3",
|
||||||
"eslint-plugin-docs": "*",
|
"eslint-plugin-docs": "*",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"jsdom": "27.0.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.25.0",
|
"stylelint": "16.25.0",
|
||||||
@@ -97,8 +98,8 @@
|
|||||||
"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.102.0",
|
"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"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.938rem;
|
font-size: 0.938rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
${({ $css }) => $css};
|
border-radius: 4px;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--c--components--button--primary-text--background--color-hover
|
--c--components--button--primary-text--background--color-hover
|
||||||
@@ -41,6 +41,7 @@ const StyledButton = styled(Button)<StyledButtonProps>`
|
|||||||
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
|
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
${({ $css }) => $css};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface DropButtonProps {
|
export interface DropButtonProps {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const Icon = ({
|
|||||||
iconName,
|
iconName,
|
||||||
disabled,
|
disabled,
|
||||||
variant = 'outlined',
|
variant = 'outlined',
|
||||||
$variation,
|
$variation = 'text',
|
||||||
...textProps
|
...textProps
|
||||||
}: IconProps) => {
|
}: IconProps) => {
|
||||||
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
||||||
@@ -41,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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { css } from 'styled-components';
|
|||||||
|
|
||||||
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { useKeyboardAction } from '@/hooks';
|
||||||
|
|
||||||
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ export const DropdownMenu = ({
|
|||||||
testId,
|
testId,
|
||||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||||
|
const keyboardAction = useKeyboardAction();
|
||||||
const [isOpen, setIsOpen] = useState(opened ?? false);
|
const [isOpen, setIsOpen] = useState(opened ?? false);
|
||||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||||
const blockButtonRef = useRef<HTMLDivElement>(null);
|
const blockButtonRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -93,6 +95,14 @@ export const DropdownMenu = ({
|
|||||||
}
|
}
|
||||||
}, [isOpen, options]);
|
}, [isOpen, options]);
|
||||||
|
|
||||||
|
const triggerOption = useCallback(
|
||||||
|
(option: DropdownMenuOption) => {
|
||||||
|
onOpenChange?.(false);
|
||||||
|
void option.callback?.();
|
||||||
|
},
|
||||||
|
[onOpenChange],
|
||||||
|
);
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
@@ -170,9 +180,9 @@ export const DropdownMenu = ({
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onOpenChange?.(false);
|
triggerOption(option);
|
||||||
void option.callback?.();
|
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={keyboardAction(() => triggerOption(option))}
|
||||||
key={option.label}
|
key={option.label}
|
||||||
$align="center"
|
$align="center"
|
||||||
$justify="space-between"
|
$justify="space-between"
|
||||||
|
|||||||
@@ -44,6 +44,11 @@
|
|||||||
contain: content;
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c__button--medium {
|
||||||
|
min-height: var(--c--components--button--medium-height);
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal
|
* Modal
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -556,8 +556,10 @@
|
|||||||
--c--theme--logo--widthHeader: 110px;
|
--c--theme--logo--widthHeader: 110px;
|
||||||
--c--theme--logo--widthFooter: 220px;
|
--c--theme--logo--widthFooter: 220px;
|
||||||
--c--theme--logo--alt: gouvernement logo;
|
--c--theme--logo--alt: gouvernement logo;
|
||||||
--c--theme--font--families--base: marianne;
|
--c--theme--font--families--base:
|
||||||
--c--theme--font--families--accent: marianne;
|
marianne, inter, roboto flex variable, sans-serif;
|
||||||
|
--c--theme--font--families--accent:
|
||||||
|
marianne, inter, roboto flex variable, sans-serif;
|
||||||
--c--components--la-gaufre: true;
|
--c--components--la-gaufre: true;
|
||||||
--c--components--home-proconnect: true;
|
--c--components--home-proconnect: true;
|
||||||
--c--components--favicon--ico: /assets/favicon-dsfr.ico;
|
--c--components--favicon--ico: /assets/favicon-dsfr.ico;
|
||||||
|
|||||||
@@ -436,7 +436,12 @@ export const tokens = {
|
|||||||
widthFooter: '220px',
|
widthFooter: '220px',
|
||||||
alt: 'Gouvernement Logo',
|
alt: 'Gouvernement Logo',
|
||||||
},
|
},
|
||||||
font: { families: { base: 'Marianne', accent: 'Marianne' } },
|
font: {
|
||||||
|
families: {
|
||||||
|
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||||
|
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
'la-gaufre': true,
|
'la-gaufre': true,
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ export interface User {
|
|||||||
short_name: string;
|
short_name: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserLight = Pick<User, 'full_name' | 'short_name'>;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function useAuthQuery(
|
|||||||
staleTime: 1000 * 60 * 15, // 15 minutes
|
staleTime: 1000 * 60 * 15, // 15 minutes
|
||||||
retry: (failureCount, error) => {
|
retry: (failureCount, error) => {
|
||||||
// we assume that a 401 means the user is not logged in
|
// we assume that a 401 means the user is not logged in
|
||||||
if (error.status == 401) {
|
if (error.status === 401) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return failureCount < DEFAULT_QUERY_RETRY;
|
return failureCount < DEFAULT_QUERY_RETRY;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Box, BoxType } from '@/components';
|
||||||
|
|
||||||
|
type AvatarSvgProps = {
|
||||||
|
initials: string;
|
||||||
|
background: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
} & BoxType;
|
||||||
|
|
||||||
|
export const AvatarSvg: React.FC<AvatarSvgProps> = ({
|
||||||
|
initials,
|
||||||
|
background,
|
||||||
|
fontFamily,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<Box
|
||||||
|
as="svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="0.5"
|
||||||
|
y="0.5"
|
||||||
|
width="23"
|
||||||
|
height="23"
|
||||||
|
rx="11.5"
|
||||||
|
ry="11.5"
|
||||||
|
fill={background}
|
||||||
|
stroke="rgba(255,255,255,0.5)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="50%"
|
||||||
|
y="50%"
|
||||||
|
dy="0.35em"
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fontWeight="600"
|
||||||
|
fill="rgba(255,255,255,0.9)"
|
||||||
|
fontFamily={fontFamily || 'Arial'}
|
||||||
|
>
|
||||||
|
{initials}
|
||||||
|
</text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { renderToStaticMarkup } from 'react-dom/server';
|
||||||
|
|
||||||
|
import { tokens } from '@/cunningham';
|
||||||
|
|
||||||
|
import { AvatarSvg } from './AvatarSvg';
|
||||||
|
|
||||||
|
const colors = tokens.themes.default.theme.colors;
|
||||||
|
|
||||||
|
const avatarsColors = [
|
||||||
|
colors['blue-500'],
|
||||||
|
colors['brown-500'],
|
||||||
|
colors['cyan-500'],
|
||||||
|
colors['gold-500'],
|
||||||
|
colors['green-500'],
|
||||||
|
colors['olive-500'],
|
||||||
|
colors['orange-500'],
|
||||||
|
colors['pink-500'],
|
||||||
|
colors['purple-500'],
|
||||||
|
colors['yellow-500'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const getColorFromName = (name: string) => {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
return avatarsColors[Math.abs(hash) % avatarsColors.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitialFromName = (name: string) => {
|
||||||
|
const splitName = name?.split(' ');
|
||||||
|
return (splitName[0]?.charAt(0) || '?') + (splitName?.[1]?.charAt(0) || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserAvatarProps = {
|
||||||
|
fullName?: string;
|
||||||
|
background?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserAvatar = ({ fullName, background }: UserAvatarProps) => {
|
||||||
|
const name = fullName?.trim() || '?';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarSvg
|
||||||
|
className="--docs--user-avatar"
|
||||||
|
initials={getInitialFromName(name).toUpperCase()}
|
||||||
|
background={background || getColorFromName(name)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const avatarUrlFromName = (
|
||||||
|
fullName?: string,
|
||||||
|
fontFamily?: string,
|
||||||
|
): string => {
|
||||||
|
const name = fullName?.trim() || '?';
|
||||||
|
const initials = getInitialFromName(name).toUpperCase();
|
||||||
|
const background = getColorFromName(name);
|
||||||
|
|
||||||
|
const svgMarkup = renderToStaticMarkup(
|
||||||
|
<AvatarSvg
|
||||||
|
className="--docs--user-avatar"
|
||||||
|
initials={initials}
|
||||||
|
background={background}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgMarkup)}`;
|
||||||
|
};
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './Auth';
|
export * from './Auth';
|
||||||
export * from './ButtonLogin';
|
export * from './ButtonLogin';
|
||||||
|
export * from './UserAvatar';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { APIError, errorCauses } from '@/api';
|
import { APIError, errorCauses } from '@/api';
|
||||||
|
import { sleep } from '@/utils';
|
||||||
|
|
||||||
interface CheckDocMediaStatusResponse {
|
interface CheckDocMediaStatusResponse {
|
||||||
file?: string;
|
file?: string;
|
||||||
@@ -25,3 +26,28 @@ export const checkDocMediaStatus = async ({
|
|||||||
|
|
||||||
return response.json() as Promise<CheckDocMediaStatusResponse>;
|
return response.json() as Promise<CheckDocMediaStatusResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload file can be analyzed on the server side,
|
||||||
|
* we had this function to wait for the analysis to be done
|
||||||
|
* before returning the file url. It will keep the loader
|
||||||
|
* on the upload button until the analysis is done.
|
||||||
|
* @param url
|
||||||
|
* @returns Promise<CheckDocMediaStatusResponse> status_code
|
||||||
|
* @description Waits for the upload to be analyzed by checking the status of the file.
|
||||||
|
*/
|
||||||
|
export const loopCheckDocMediaStatus = async (
|
||||||
|
url: string,
|
||||||
|
): Promise<CheckDocMediaStatusResponse> => {
|
||||||
|
const SLEEP_TIME = 5000;
|
||||||
|
const response = await checkDocMediaStatus({
|
||||||
|
urlMedia: url,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 'ready') {
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
await sleep(SLEEP_TIME);
|
||||||
|
return await loopCheckDocMediaStatus(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
+92
-35
@@ -12,17 +12,16 @@ import { BlockNoteView } from '@blocknote/mantine';
|
|||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
import { useCreateBlockNote } from '@blocknote/react';
|
import { useCreateBlockNote } from '@blocknote/react';
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from 'styled-components';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, TextErrors } from '@/components';
|
import { Box, TextErrors } from '@/components';
|
||||||
import {
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
Doc,
|
import { Doc, useProviderStore } from '@/docs/doc-management';
|
||||||
useIsCollaborativeEditable,
|
import { avatarUrlFromName, useAuth } from '@/features/auth';
|
||||||
useProviderStore,
|
import { useResponsiveStore } from '@/stores';
|
||||||
} from '@/docs/doc-management';
|
|
||||||
import { useAuth } from '@/features/auth';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useHeadings,
|
useHeadings,
|
||||||
@@ -38,6 +37,7 @@ import { randomColor } from '../utils';
|
|||||||
|
|
||||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||||
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
|
||||||
|
import { cssComments, useComments } from './comments/';
|
||||||
import {
|
import {
|
||||||
AccessibleImageBlock,
|
AccessibleImageBlock,
|
||||||
CalloutBlock,
|
CalloutBlock,
|
||||||
@@ -83,30 +83,37 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { setEditor } = useEditorStore();
|
const { setEditor } = useEditorStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { themeTokens } = useCunninghamTheme();
|
||||||
|
const { isDesktop } = useResponsiveStore();
|
||||||
const { isSynced: isConnectedToCollabServer } = useProviderStore();
|
const { isSynced: isConnectedToCollabServer } = useProviderStore();
|
||||||
|
const refEditorContainer = useRef<HTMLDivElement>(null);
|
||||||
|
const canSeeComment = doc.abilities.comment && isDesktop;
|
||||||
|
|
||||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
|
||||||
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
|
||||||
const isDeletedDoc = !!doc.deleted_at;
|
|
||||||
|
|
||||||
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
|
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const lang = i18n.resolvedLanguage;
|
const lang = i18n.resolvedLanguage;
|
||||||
|
|
||||||
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
|
||||||
|
|
||||||
const collabName = readOnly
|
const collabName = user?.full_name || user?.email;
|
||||||
? 'Reader'
|
const cursorName = collabName || t('Anonymous');
|
||||||
: user?.full_name || user?.email || t('Anonymous');
|
|
||||||
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
||||||
|
|
||||||
|
const threadStore = useComments(doc.id, canSeeComment, user);
|
||||||
|
|
||||||
|
const currentUserAvatarUrl = useMemo(() => {
|
||||||
|
if (canSeeComment) {
|
||||||
|
return avatarUrlFromName(collabName, themeTokens?.font?.families?.base);
|
||||||
|
}
|
||||||
|
}, [canSeeComment, collabName, themeTokens?.font?.families?.base]);
|
||||||
|
|
||||||
const editor: DocsBlockNoteEditor = useCreateBlockNote(
|
const editor: DocsBlockNoteEditor = useCreateBlockNote(
|
||||||
{
|
{
|
||||||
collaboration: {
|
collaboration: {
|
||||||
provider,
|
provider: provider,
|
||||||
fragment: provider.document.getXmlFragment('document-store'),
|
fragment: provider.document.getXmlFragment('document-store'),
|
||||||
user: {
|
user: {
|
||||||
name: collabName,
|
name: cursorName,
|
||||||
color: randomColor(),
|
color: randomColor(),
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -117,10 +124,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
renderCursor: (user: { color: string; name: string }) => {
|
renderCursor: (user: { color: string; name: string }) => {
|
||||||
const cursorElement = document.createElement('span');
|
const cursorElement = document.createElement('span');
|
||||||
|
|
||||||
if (user.name === 'Reader') {
|
|
||||||
return cursorElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursorElement.classList.add('collaboration-cursor-custom__base');
|
cursorElement.classList.add('collaboration-cursor-custom__base');
|
||||||
const caretElement = document.createElement('span');
|
const caretElement = document.createElement('span');
|
||||||
caretElement.classList.add('collaboration-cursor-custom__caret');
|
caretElement.classList.add('collaboration-cursor-custom__caret');
|
||||||
@@ -151,11 +154,28 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
},
|
},
|
||||||
showCursorLabels: showCursorLabels as 'always' | 'activity',
|
showCursorLabels: showCursorLabels as 'always' | 'activity',
|
||||||
},
|
},
|
||||||
|
comments: { threadStore },
|
||||||
dictionary: {
|
dictionary: {
|
||||||
...locales[lang as keyof typeof locales],
|
...locales[lang as keyof typeof locales],
|
||||||
multi_column:
|
multi_column:
|
||||||
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
|
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
|
||||||
},
|
},
|
||||||
|
resolveUsers: async (userIds) => {
|
||||||
|
return Promise.resolve(
|
||||||
|
userIds.map((encodedURIUserId) => {
|
||||||
|
const fullName = decodeURIComponent(encodedURIUserId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: encodedURIUserId,
|
||||||
|
username: fullName || t('Anonymous'),
|
||||||
|
avatarUrl: avatarUrlFromName(
|
||||||
|
fullName,
|
||||||
|
themeTokens?.font?.families?.base,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
tables: {
|
tables: {
|
||||||
splitCells: true,
|
splitCells: true,
|
||||||
cellBackgroundColor: true,
|
cellBackgroundColor: true,
|
||||||
@@ -165,11 +185,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
uploadFile,
|
uploadFile,
|
||||||
schema: blockNoteSchema,
|
schema: blockNoteSchema,
|
||||||
},
|
},
|
||||||
[collabName, lang, provider, uploadFile],
|
[cursorName, lang, provider, uploadFile, threadStore],
|
||||||
);
|
);
|
||||||
|
|
||||||
useHeadings(editor);
|
useHeadings(editor);
|
||||||
useShortcuts(editor);
|
|
||||||
|
useShortcuts(editor, refEditorContainer.current);
|
||||||
|
|
||||||
useUploadStatus(editor);
|
useUploadStatus(editor);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -182,10 +204,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
$padding={{ top: 'md' }}
|
ref={refEditorContainer}
|
||||||
$background="white"
|
$css={css`
|
||||||
$css={cssEditor(readOnly, isDeletedDoc)}
|
${cssEditor};
|
||||||
className="--docs--editor-container"
|
${cssComments(canSeeComment, currentUserAvatarUrl)}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{errorAttachment && (
|
{errorAttachment && (
|
||||||
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
|
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
|
||||||
@@ -196,13 +219,14 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<BlockNoteView
|
<BlockNoteView
|
||||||
|
className="--docs--main-editor"
|
||||||
editor={editor}
|
editor={editor}
|
||||||
formattingToolbar={false}
|
formattingToolbar={false}
|
||||||
slashMenu={false}
|
slashMenu={false}
|
||||||
editable={!readOnly}
|
|
||||||
theme="light"
|
theme="light"
|
||||||
|
comments={canSeeComment}
|
||||||
|
aria-label={t('Document editor')}
|
||||||
>
|
>
|
||||||
<BlockNoteSuggestionMenu />
|
<BlockNoteSuggestionMenu />
|
||||||
<BlockNoteToolbar />
|
<BlockNoteToolbar />
|
||||||
@@ -211,14 +235,19 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface BlockNoteEditorVersionProps {
|
interface BlockNoteReaderProps {
|
||||||
|
docId: Doc['id'];
|
||||||
initialContent: Y.XmlFragment;
|
initialContent: Y.XmlFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockNoteEditorVersion = ({
|
export const BlockNoteReader = ({
|
||||||
|
docId,
|
||||||
initialContent,
|
initialContent,
|
||||||
}: BlockNoteEditorVersionProps) => {
|
}: BlockNoteReaderProps) => {
|
||||||
const readOnly = true;
|
const { user } = useAuth();
|
||||||
|
const { setEditor } = useEditorStore();
|
||||||
|
const threadStore = useComments(docId, false, user);
|
||||||
|
const { t } = useTranslation();
|
||||||
const editor = useCreateBlockNote(
|
const editor = useCreateBlockNote(
|
||||||
{
|
{
|
||||||
collaboration: {
|
collaboration: {
|
||||||
@@ -230,13 +259,41 @@ export const BlockNoteEditorVersion = ({
|
|||||||
provider: undefined,
|
provider: undefined,
|
||||||
},
|
},
|
||||||
schema: blockNoteSchema,
|
schema: blockNoteSchema,
|
||||||
|
comments: { threadStore },
|
||||||
|
resolveUsers: async () => {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[initialContent],
|
[initialContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditor(editor);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setEditor(undefined);
|
||||||
|
};
|
||||||
|
}, [setEditor, editor]);
|
||||||
|
|
||||||
|
useHeadings(editor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box $css={cssEditor(readOnly, true)} className="--docs--editor-container">
|
<Box
|
||||||
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
|
$css={css`
|
||||||
|
${cssEditor};
|
||||||
|
${cssComments(false)}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<BlockNoteView
|
||||||
|
className="--docs--main-editor"
|
||||||
|
editor={editor}
|
||||||
|
editable={false}
|
||||||
|
theme="light"
|
||||||
|
aria-label={t('Document version viewer')}
|
||||||
|
formattingToolbar={false}
|
||||||
|
slashMenu={false}
|
||||||
|
comments={false}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-1
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { useConfig } from '@/core/config/api';
|
import { useConfig } from '@/core/config/api';
|
||||||
|
|
||||||
|
import { CommentToolbarButton } from '../comments/CommentToolbarButton';
|
||||||
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
|
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
|
||||||
|
|
||||||
import { AIGroupButton } from './AIButton';
|
import { AIGroupButton } from './AIButton';
|
||||||
@@ -25,10 +26,12 @@ export const BlockNoteToolbar = () => {
|
|||||||
const { data: conf } = useConfig();
|
const { data: conf } = useConfig();
|
||||||
|
|
||||||
const toolbarItems = useMemo(() => {
|
const toolbarItems = useMemo(() => {
|
||||||
const toolbarItems = getFormattingToolbarItems([
|
let toolbarItems = getFormattingToolbarItems([
|
||||||
...blockTypeSelectItems(dict),
|
...blockTypeSelectItems(dict),
|
||||||
getCalloutFormattingToolbarItems(t),
|
getCalloutFormattingToolbarItems(t),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Find the index of the file download button
|
||||||
const fileDownloadButtonIndex = toolbarItems.findIndex(
|
const fileDownloadButtonIndex = toolbarItems.findIndex(
|
||||||
(item) =>
|
(item) =>
|
||||||
typeof item === 'object' &&
|
typeof item === 'object' &&
|
||||||
@@ -36,6 +39,8 @@ export const BlockNoteToolbar = () => {
|
|||||||
'key' in item &&
|
'key' in item &&
|
||||||
(item as { key: string }).key === 'fileDownloadButton',
|
(item as { key: string }).key === 'fileDownloadButton',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Replace the default file download button with our custom FileDownloadButton
|
||||||
if (fileDownloadButtonIndex !== -1) {
|
if (fileDownloadButtonIndex !== -1) {
|
||||||
toolbarItems.splice(
|
toolbarItems.splice(
|
||||||
fileDownloadButtonIndex,
|
fileDownloadButtonIndex,
|
||||||
@@ -50,12 +55,22 @@ export const BlockNoteToolbar = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove default Comment button
|
||||||
|
toolbarItems = toolbarItems.filter((item) => {
|
||||||
|
if (typeof item === 'object' && item !== null && 'key' in item) {
|
||||||
|
return item.key !== 'addCommentButton';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return toolbarItems;
|
return toolbarItems;
|
||||||
}, [dict, t]);
|
}, [dict, t]);
|
||||||
|
|
||||||
const formattingToolbar = useCallback(() => {
|
const formattingToolbar = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<FormattingToolbar>
|
<FormattingToolbar>
|
||||||
|
<CommentToolbarButton />
|
||||||
|
|
||||||
{toolbarItems}
|
{toolbarItems}
|
||||||
|
|
||||||
{/* Extra button to do some AI powered actions */}
|
{/* Extra button to do some AI powered actions */}
|
||||||
|
|||||||
+16
-3
@@ -6,6 +6,9 @@ import {
|
|||||||
import { forEach, isArray } from 'lodash';
|
import { forEach, isArray } from 'lodash';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
|
import { Text } from '@/components';
|
||||||
|
|
||||||
type Block = {
|
type Block = {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -83,8 +86,18 @@ export function MarkdownButton() {
|
|||||||
mainTooltip={t('Convert Markdown')}
|
mainTooltip={t('Convert Markdown')}
|
||||||
onClick={handleConvertMarkdown}
|
onClick={handleConvertMarkdown}
|
||||||
className="--docs--editor-markdown-button"
|
className="--docs--editor-markdown-button"
|
||||||
>
|
label="M"
|
||||||
M
|
icon={
|
||||||
</Components.FormattingToolbar.Button>
|
<Text
|
||||||
|
aria-hidden={true}
|
||||||
|
$css={css`
|
||||||
|
font-family: var(--c--theme--font--families--base);
|
||||||
|
`}
|
||||||
|
$weight="bold"
|
||||||
|
>
|
||||||
|
M
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,87 @@
|
|||||||
import { Loader } from '@openfun/cunningham-react';
|
import clsx from 'clsx';
|
||||||
import { useRouter } from 'next/router';
|
import { useEffect } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
import * as Y from 'yjs';
|
|
||||||
|
|
||||||
import { Box, Loading, Text, TextErrors } from '@/components';
|
import { Box, Loading } from '@/components';
|
||||||
import { DocHeader, DocVersionHeader } from '@/docs/doc-header/';
|
import { DocHeader } from '@/docs/doc-header/';
|
||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
base64ToBlocknoteXmlFragment,
|
useIsCollaborativeEditable,
|
||||||
useProviderStore,
|
useProviderStore,
|
||||||
} from '@/docs/doc-management';
|
} from '@/docs/doc-management';
|
||||||
import { TableContent } from '@/docs/doc-table-content/';
|
import { TableContent } from '@/docs/doc-table-content/';
|
||||||
import { Versions, useDocVersion } from '@/docs/doc-versioning/';
|
|
||||||
import { useSkeletonStore } from '@/features/skeletons';
|
import { useSkeletonStore } from '@/features/skeletons';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor';
|
||||||
|
|
||||||
|
interface DocEditorContainerProps {
|
||||||
|
docHeader: React.ReactNode;
|
||||||
|
docEditor: React.ReactNode;
|
||||||
|
isDeletedDoc: boolean;
|
||||||
|
readOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocEditorContainer = ({
|
||||||
|
docHeader,
|
||||||
|
docEditor,
|
||||||
|
isDeletedDoc,
|
||||||
|
readOnly,
|
||||||
|
}: DocEditorContainerProps) => {
|
||||||
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
$maxWidth="868px"
|
||||||
|
$width="100%"
|
||||||
|
$height="100%"
|
||||||
|
className="--docs--doc-editor"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
|
||||||
|
className="--docs--doc-editor-header"
|
||||||
|
>
|
||||||
|
{docHeader}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
$direction="row"
|
||||||
|
$width="100%"
|
||||||
|
$css="overflow-x: clip; flex: 1;"
|
||||||
|
$position="relative"
|
||||||
|
className="--docs--doc-editor-content"
|
||||||
|
>
|
||||||
|
<Box $css="flex:1;" $position="relative" $width="100%">
|
||||||
|
<Box
|
||||||
|
$padding={{ top: 'md', bottom: '2rem' }}
|
||||||
|
$background="white"
|
||||||
|
className={clsx('--docs--editor-container', {
|
||||||
|
'--docs--doc-readonly': readOnly,
|
||||||
|
'--docs--doc-deleted': isDeletedDoc,
|
||||||
|
})}
|
||||||
|
$height="100%"
|
||||||
|
>
|
||||||
|
{docEditor}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface DocEditorProps {
|
interface DocEditorProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
versionId?: Versions['version_id'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
const isVersion = !!versionId && typeof versionId === 'string';
|
|
||||||
const { provider, isReady } = useProviderStore();
|
const { provider, isReady } = useProviderStore();
|
||||||
|
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||||
|
const isDeletedDoc = !!doc.deleted_at;
|
||||||
|
const readOnly =
|
||||||
|
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
|
||||||
const { setIsSkeletonVisible } = useSkeletonStore();
|
const { setIsSkeletonVisible } = useSkeletonStore();
|
||||||
const isProviderReady = isReady && provider;
|
const isProviderReady = isReady && provider;
|
||||||
|
|
||||||
@@ -42,8 +97,9 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDesktop && !isVersion && (
|
{isDesktop && (
|
||||||
<Box
|
<Box
|
||||||
|
$height="100vh"
|
||||||
$position="absolute"
|
$position="absolute"
|
||||||
$css={css`
|
$css={css`
|
||||||
top: 72px;
|
top: 72px;
|
||||||
@@ -53,102 +109,23 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
|||||||
<TableContent />
|
<TableContent />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box
|
<DocEditorContainer
|
||||||
$maxWidth="868px"
|
docHeader={<DocHeader doc={doc} />}
|
||||||
$width="100%"
|
docEditor={
|
||||||
$height="100%"
|
readOnly ? (
|
||||||
className="--docs--doc-editor"
|
<BlockNoteReader
|
||||||
>
|
initialContent={provider.document.getXmlFragment(
|
||||||
<Box
|
'document-store',
|
||||||
$padding={{ horizontal: isDesktop ? '54px' : 'base' }}
|
)}
|
||||||
className="--docs--doc-editor-header"
|
docId={doc.id}
|
||||||
>
|
/>
|
||||||
{isVersion ? <DocVersionHeader /> : <DocHeader doc={doc} />}
|
) : (
|
||||||
</Box>
|
<BlockNoteEditor doc={doc} provider={provider} />
|
||||||
|
)
|
||||||
<Box
|
}
|
||||||
$direction="row"
|
isDeletedDoc={isDeletedDoc}
|
||||||
$width="100%"
|
readOnly={readOnly}
|
||||||
$css="overflow-x: clip; flex: 1;"
|
/>
|
||||||
$position="relative"
|
|
||||||
className="--docs--doc-editor-content"
|
|
||||||
>
|
|
||||||
<Box $css="flex:1;" $position="relative" $width="100%">
|
|
||||||
{isVersion ? (
|
|
||||||
<DocVersionEditor docId={doc.id} versionId={versionId} />
|
|
||||||
) : (
|
|
||||||
<BlockNoteEditor doc={doc} provider={provider} />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DocVersionEditorProps {
|
|
||||||
docId: Doc['id'];
|
|
||||||
versionId: Versions['version_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocVersionEditor = ({
|
|
||||||
docId,
|
|
||||||
versionId,
|
|
||||||
}: DocVersionEditorProps) => {
|
|
||||||
const {
|
|
||||||
data: version,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useDocVersion({
|
|
||||||
docId,
|
|
||||||
versionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { replace } = useRouter();
|
|
||||||
const [initialContent, setInitialContent] = useState<Y.XmlFragment>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!version?.content) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInitialContent(base64ToBlocknoteXmlFragment(version.content));
|
|
||||||
}, [version?.content]);
|
|
||||||
|
|
||||||
if (isError && error) {
|
|
||||||
if (error.status === 404) {
|
|
||||||
void replace(`/404`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box $margin="large" className="--docs--doc-version-editor-error">
|
|
||||||
<TextErrors
|
|
||||||
causes={error.cause}
|
|
||||||
icon={
|
|
||||||
error.status === 502 ? (
|
|
||||||
<Text
|
|
||||||
className="material-icons"
|
|
||||||
$theme="danger"
|
|
||||||
aria-hidden={true}
|
|
||||||
>
|
|
||||||
wifi_off
|
|
||||||
</Text>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading || !version || !initialContent) {
|
|
||||||
return (
|
|
||||||
<Box $align="center" $justify="center" $height="100%">
|
|
||||||
<Loader />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <BlockNoteEditorVersion initialContent={initialContent} />;
|
|
||||||
};
|
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
import { useBlockNoteEditor, useComponentsContext } from '@blocknote/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
|
import { Box, Icon } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { useDocStore } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DocsBlockSchema,
|
||||||
|
DocsInlineContentSchema,
|
||||||
|
DocsStyleSchema,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
export const CommentToolbarButton = () => {
|
||||||
|
const Components = useComponentsContext();
|
||||||
|
const { currentDoc } = useDocStore();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
|
const editor = useBlockNoteEditor<
|
||||||
|
DocsBlockSchema,
|
||||||
|
DocsInlineContentSchema,
|
||||||
|
DocsStyleSchema
|
||||||
|
>();
|
||||||
|
|
||||||
|
if (!editor.isEditable || !Components || !currentDoc?.abilities.comment) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box $direction="row" className="--docs--comment-toolbar-button">
|
||||||
|
<Components.Generic.Toolbar.Button
|
||||||
|
className="bn-button"
|
||||||
|
onClick={() => {
|
||||||
|
editor.comments?.startPendingComment();
|
||||||
|
}}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$direction="row"
|
||||||
|
$align="center"
|
||||||
|
$gap={spacingsTokens['xs']}
|
||||||
|
$padding={{ right: '2xs' }}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="comment"
|
||||||
|
className="--docs--icon-bg"
|
||||||
|
$theme="greyscale"
|
||||||
|
$variation="600"
|
||||||
|
$padding="0.15rem"
|
||||||
|
$size="16px"
|
||||||
|
$color={colorsTokens['greyscale-600']}
|
||||||
|
/>
|
||||||
|
{t('Comment')}
|
||||||
|
</Box>
|
||||||
|
</Components.Generic.Toolbar.Button>
|
||||||
|
<Box
|
||||||
|
$background={colorsTokens['greyscale-100']}
|
||||||
|
$width="1px"
|
||||||
|
$height="70%"
|
||||||
|
$margin={{ left: '2px' }}
|
||||||
|
$css={css`
|
||||||
|
align-self: center;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
+569
@@ -0,0 +1,569 @@
|
|||||||
|
import { CommentBody, ThreadStore } from '@blocknote/core/comments';
|
||||||
|
import type { Awareness } from 'y-protocols/awareness';
|
||||||
|
|
||||||
|
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||||
|
import { Doc } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
import { useEditorStore } from '../../stores';
|
||||||
|
|
||||||
|
import { DocsThreadStoreAuth } from './DocsThreadStoreAuth';
|
||||||
|
import {
|
||||||
|
ClientCommentData,
|
||||||
|
ClientThreadData,
|
||||||
|
ServerComment,
|
||||||
|
ServerReaction,
|
||||||
|
ServerThread,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
type ServerThreadListResponse = APIList<ServerThread>;
|
||||||
|
|
||||||
|
export class DocsThreadStore extends ThreadStore {
|
||||||
|
protected static COMMENTS_PING = 'commentsPing';
|
||||||
|
protected threads: Map<string, ClientThreadData> = new Map();
|
||||||
|
private subscribers = new Set<
|
||||||
|
(threads: Map<string, ClientThreadData>) => void
|
||||||
|
>();
|
||||||
|
private awareness?: Awareness;
|
||||||
|
private lastPingAt = 0;
|
||||||
|
private pingTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected docId: Doc['id'],
|
||||||
|
awareness: Awareness | undefined,
|
||||||
|
protected docAuth: DocsThreadStoreAuth,
|
||||||
|
) {
|
||||||
|
super(docAuth);
|
||||||
|
|
||||||
|
if (docAuth.canSee) {
|
||||||
|
this.awareness = awareness;
|
||||||
|
this.awareness?.on('update', this.onAwarenessUpdate);
|
||||||
|
void this.refreshThreads();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.awareness?.off('update', this.onAwarenessUpdate);
|
||||||
|
if (this.pingTimer) {
|
||||||
|
clearTimeout(this.pingTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAwarenessUpdate = async ({
|
||||||
|
added,
|
||||||
|
updated,
|
||||||
|
}: {
|
||||||
|
added: number[];
|
||||||
|
updated: number[];
|
||||||
|
}) => {
|
||||||
|
if (!this.awareness) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const states = this.awareness.getStates();
|
||||||
|
const listClientIds = [...added, ...updated];
|
||||||
|
for (const clientId of listClientIds) {
|
||||||
|
// Skip our own client ID
|
||||||
|
if (clientId === this.awareness.clientID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = states.get(clientId) as
|
||||||
|
| {
|
||||||
|
[DocsThreadStore.COMMENTS_PING]?: {
|
||||||
|
at: number;
|
||||||
|
docId: string;
|
||||||
|
isResolving: boolean;
|
||||||
|
threadId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const ping = state?.commentsPing;
|
||||||
|
|
||||||
|
// Skip if no ping information is available
|
||||||
|
if (!ping) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the document ID doesn't match
|
||||||
|
if (ping.docId !== this.docId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the ping timestamp is past
|
||||||
|
if (ping.at <= this.lastPingAt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastPingAt = ping.at;
|
||||||
|
|
||||||
|
// If we know the threadId, schedule a targeted refresh. Otherwise, fall back to full refresh.
|
||||||
|
if (ping.threadId) {
|
||||||
|
await this.refreshThread(ping.threadId);
|
||||||
|
} else {
|
||||||
|
await this.refreshThreads();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To ping the other clients for updates on a specific thread
|
||||||
|
* @param threadId
|
||||||
|
*/
|
||||||
|
private ping(threadId?: string) {
|
||||||
|
this.awareness?.setLocalStateField(DocsThreadStore.COMMENTS_PING, {
|
||||||
|
at: Date.now(),
|
||||||
|
docId: this.docId,
|
||||||
|
threadId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all subscribers about the current thread state
|
||||||
|
*/
|
||||||
|
private notifySubscribers() {
|
||||||
|
// Always emit a new Map reference to help consumers detect changes
|
||||||
|
const threads = new Map(this.threads);
|
||||||
|
this.subscribers.forEach((cb) => {
|
||||||
|
try {
|
||||||
|
cb(threads);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('DocsThreadStore subscriber threw', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private upsertClientThreadData(thread: ClientThreadData) {
|
||||||
|
const next = new Map(this.threads);
|
||||||
|
next.set(thread.id, thread);
|
||||||
|
this.threads = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeThread(threadId: string) {
|
||||||
|
const next = new Map(this.threads);
|
||||||
|
next.delete(threadId);
|
||||||
|
this.threads = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To subscribe to thread updates
|
||||||
|
* @param cb
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public subscribe(cb: (threads: Map<string, ClientThreadData>) => void) {
|
||||||
|
if (!this.docAuth.canSee) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscribers.add(cb);
|
||||||
|
|
||||||
|
// Emit initial state asynchronously to avoid running during editor init
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.subscribers.has(cb)) {
|
||||||
|
cb(this.getThreads());
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.subscribers.delete(cb);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public addThreadToDocument = (options: {
|
||||||
|
threadId: string;
|
||||||
|
selection: {
|
||||||
|
prosemirror: {
|
||||||
|
head: number;
|
||||||
|
anchor: number;
|
||||||
|
};
|
||||||
|
yjs: {
|
||||||
|
head: unknown;
|
||||||
|
anchor: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const { threadId } = options;
|
||||||
|
const { editor } = useEditorStore.getState();
|
||||||
|
|
||||||
|
// Should not happen
|
||||||
|
if (!editor) {
|
||||||
|
console.warn('Editor to add thread not ready');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
editor._tiptapEditor
|
||||||
|
.chain()
|
||||||
|
.focus?.()
|
||||||
|
.setMark?.('comment', { orphan: false, threadId })
|
||||||
|
.run?.();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
public createThread = async (options: {
|
||||||
|
initialComment: {
|
||||||
|
body: CommentBody;
|
||||||
|
metadata?: unknown;
|
||||||
|
};
|
||||||
|
metadata?: unknown;
|
||||||
|
}) => {
|
||||||
|
const response = await fetchAPI(`documents/${this.docId}/threads/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
body: options.initialComment.body,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to create thread in document',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thread = (await response.json()) as ServerThread;
|
||||||
|
const threadData: ClientThreadData = serverThreadToClientThread(thread);
|
||||||
|
this.upsertClientThreadData(threadData);
|
||||||
|
this.notifySubscribers();
|
||||||
|
this.ping(threadData.id);
|
||||||
|
return threadData;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getThread(threadId: string) {
|
||||||
|
const thread = this.threads.get(threadId);
|
||||||
|
if (!thread) {
|
||||||
|
throw new Error('Thread not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getThreads(): Map<string, ClientThreadData> {
|
||||||
|
if (!this.docAuth.canSee) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refreshThread(threadId: string) {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${threadId}/`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// If not OK and 404, the thread might have been deleted but the
|
||||||
|
// thread modal is still open, so we close it to avoid side effects
|
||||||
|
if (response.status === 404) {
|
||||||
|
// use escape key event to close the thread modal
|
||||||
|
document.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
key: 'Escape',
|
||||||
|
code: 'Escape',
|
||||||
|
keyCode: 27,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.refreshThreads();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
`Failed to fetch thread ${threadId}`,
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverThread = (await response.json()) as ServerThread;
|
||||||
|
|
||||||
|
const clientThread = serverThreadToClientThread(serverThread);
|
||||||
|
this.upsertClientThreadData(clientThread);
|
||||||
|
this.notifySubscribers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refreshThreads(): Promise<void> {
|
||||||
|
const response = await fetchAPI(`documents/${this.docId}/threads/`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to get threads in document',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const threads = (await response.json()) as ServerThreadListResponse;
|
||||||
|
const next = new Map<string, ClientThreadData>();
|
||||||
|
threads.results.forEach((thread) => {
|
||||||
|
const threadData: ClientThreadData = serverThreadToClientThread(thread);
|
||||||
|
next.set(thread.id, threadData);
|
||||||
|
});
|
||||||
|
this.threads = next;
|
||||||
|
this.notifySubscribers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public addComment = async (options: {
|
||||||
|
comment: {
|
||||||
|
body: CommentBody;
|
||||||
|
metadata?: unknown;
|
||||||
|
};
|
||||||
|
threadId: string;
|
||||||
|
}) => {
|
||||||
|
const { threadId } = options;
|
||||||
|
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${threadId}/comments/`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
body: options.comment.body,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError('Failed to add comment ', await errorCauses(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
const comment = (await response.json()) as ServerComment;
|
||||||
|
|
||||||
|
// Optimistically update local thread with new comment
|
||||||
|
const existing = this.threads.get(threadId);
|
||||||
|
if (existing) {
|
||||||
|
const updated: ClientThreadData = {
|
||||||
|
...existing,
|
||||||
|
updatedAt: new Date(comment.updated_at || comment.created_at),
|
||||||
|
comments: [...existing.comments, serverCommentToClientComment(comment)],
|
||||||
|
};
|
||||||
|
this.upsertClientThreadData(updated);
|
||||||
|
this.notifySubscribers();
|
||||||
|
} else {
|
||||||
|
// Fallback to fetching the thread if we don't have it locally
|
||||||
|
await this.refreshThread(threadId);
|
||||||
|
}
|
||||||
|
this.ping(threadId);
|
||||||
|
return serverCommentToClientComment(comment);
|
||||||
|
};
|
||||||
|
|
||||||
|
public updateComment = async (options: {
|
||||||
|
comment: {
|
||||||
|
body: CommentBody;
|
||||||
|
metadata?: unknown;
|
||||||
|
};
|
||||||
|
threadId: string;
|
||||||
|
commentId: string;
|
||||||
|
}) => {
|
||||||
|
const { threadId, commentId, comment } = options;
|
||||||
|
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${threadId}/comments/${commentId}/`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
body: comment.body,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to add thread to document',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshThread(threadId);
|
||||||
|
this.ping(threadId);
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
public deleteComment = async (options: {
|
||||||
|
threadId: string;
|
||||||
|
commentId: string;
|
||||||
|
softDelete?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { threadId, commentId } = options;
|
||||||
|
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${threadId}/comments/${commentId}/`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to delete comment',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimistically remove the comment locally if we have the thread
|
||||||
|
const existing = this.threads.get(threadId);
|
||||||
|
if (existing) {
|
||||||
|
const updated: ClientThreadData = {
|
||||||
|
...existing,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
comments: existing.comments.filter((c) => c.id !== commentId),
|
||||||
|
};
|
||||||
|
this.upsertClientThreadData(updated);
|
||||||
|
this.notifySubscribers();
|
||||||
|
} else {
|
||||||
|
// Fallback to fetching the thread
|
||||||
|
await this.refreshThread(threadId);
|
||||||
|
}
|
||||||
|
this.ping(threadId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI not implemented
|
||||||
|
* @param _options
|
||||||
|
*/
|
||||||
|
public deleteThread = async (_options: { threadId: string }) => {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${_options.threadId}/`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to delete thread',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove locally and notify; no need to refetch everything
|
||||||
|
this.removeThread(_options.threadId);
|
||||||
|
this.notifySubscribers();
|
||||||
|
this.ping(_options.threadId);
|
||||||
|
};
|
||||||
|
|
||||||
|
public resolveThread = async (_options: { threadId: string }) => {
|
||||||
|
const { threadId } = _options;
|
||||||
|
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${threadId}/resolve/`,
|
||||||
|
{ method: 'POST' },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to resolve thread',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshThreads();
|
||||||
|
this.ping(threadId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Todo: Not implemented backend side
|
||||||
|
* @returns
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public unresolveThread = async (_options: { threadId: string }) => {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${_options.threadId}/unresolve/`,
|
||||||
|
{ method: 'POST' },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to unresolve thread',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshThread(_options.threadId);
|
||||||
|
this.ping(_options.threadId);
|
||||||
|
};
|
||||||
|
|
||||||
|
public addReaction = async (options: {
|
||||||
|
threadId: string;
|
||||||
|
commentId: string;
|
||||||
|
emoji: string;
|
||||||
|
}) => {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${options.threadId}/comments/${options.commentId}/reactions/`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ emoji: options.emoji }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to add reaction to comment',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshThread(options.threadId);
|
||||||
|
this.notifySubscribers();
|
||||||
|
this.ping(options.threadId);
|
||||||
|
};
|
||||||
|
|
||||||
|
public deleteReaction = async (options: {
|
||||||
|
threadId: string;
|
||||||
|
commentId: string;
|
||||||
|
emoji: string;
|
||||||
|
}) => {
|
||||||
|
const response = await fetchAPI(
|
||||||
|
`documents/${this.docId}/threads/${options.threadId}/comments/${options.commentId}/reactions/`,
|
||||||
|
{ method: 'DELETE', body: JSON.stringify({ emoji: options.emoji }) },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to delete reaction from comment',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.refreshThread(options.threadId);
|
||||||
|
this.notifySubscribers();
|
||||||
|
this.ping(options.threadId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverReactionToReactionData = (r: ServerReaction) => {
|
||||||
|
return {
|
||||||
|
emoji: r.emoji,
|
||||||
|
createdAt: new Date(r.created_at),
|
||||||
|
userIds: r.users?.map((user) =>
|
||||||
|
encodeURIComponent(user.full_name || ''),
|
||||||
|
) || [''],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverCommentToClientComment = (c: ServerComment): ClientCommentData => ({
|
||||||
|
type: 'comment',
|
||||||
|
id: c.id,
|
||||||
|
userId: encodeURIComponent(c.user?.full_name || ''),
|
||||||
|
body: c.body,
|
||||||
|
createdAt: new Date(c.created_at),
|
||||||
|
updatedAt: new Date(c.updated_at),
|
||||||
|
reactions: (c.reactions ?? []).map(serverReactionToReactionData),
|
||||||
|
metadata: { abilities: c.abilities },
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverThreadToClientThread = (t: ServerThread): ClientThreadData => ({
|
||||||
|
type: 'thread',
|
||||||
|
id: t.id,
|
||||||
|
createdAt: new Date(t.created_at),
|
||||||
|
updatedAt: new Date(t.updated_at),
|
||||||
|
comments: (t.comments ?? []).map(serverCommentToClientComment),
|
||||||
|
resolved: t.resolved,
|
||||||
|
resolvedUpdatedAt: t.resolved_updated_at
|
||||||
|
? new Date(t.resolved_updated_at)
|
||||||
|
: undefined,
|
||||||
|
resolvedBy: t.resolved_by || undefined,
|
||||||
|
metadata: { abilities: t.abilities, metadata: t.metadata },
|
||||||
|
});
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
import { ThreadStoreAuth } from '@blocknote/core/comments';
|
||||||
|
|
||||||
|
import { ClientCommentData, ClientThreadData } from './types';
|
||||||
|
|
||||||
|
export class DocsThreadStoreAuth extends ThreadStoreAuth {
|
||||||
|
constructor(
|
||||||
|
private readonly userId: string,
|
||||||
|
public canSee: boolean,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canCreateThread(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
canAddComment(_thread: ClientThreadData): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
canUpdateComment(comment: ClientCommentData): boolean {
|
||||||
|
if (
|
||||||
|
comment.metadata.abilities.partial_update &&
|
||||||
|
comment.userId === this.userId
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteComment(comment: ClientCommentData): boolean {
|
||||||
|
if (comment.metadata.abilities.destroy) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteThread(thread: ClientThreadData): boolean {
|
||||||
|
if (thread.metadata.abilities.destroy) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canResolveThread(thread: ClientThreadData): boolean {
|
||||||
|
if (thread.metadata.abilities.resolve) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not implemented backend side
|
||||||
|
* @param _thread
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
canUnresolveThread(_thread: ClientThreadData): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
canAddReaction(comment: ClientCommentData, emoji?: string): boolean {
|
||||||
|
if (!comment.metadata.abilities.reactions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !comment.reactions.some(
|
||||||
|
(reaction) =>
|
||||||
|
reaction.emoji === emoji && reaction.userIds.includes(this.userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeleteReaction(comment: ClientCommentData, emoji?: string): boolean {
|
||||||
|
if (!comment.metadata.abilities.reactions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment.reactions.some(
|
||||||
|
(reaction) =>
|
||||||
|
reaction.emoji === emoji && reaction.userIds.includes(this.userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './CommentToolbarButton';
|
||||||
|
export * from './styles';
|
||||||
|
export * from './useComments';
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
|
export const cssComments = (
|
||||||
|
canSeeComment: boolean,
|
||||||
|
currentUserAvatarUrl?: string,
|
||||||
|
) => css`
|
||||||
|
& .--docs--main-editor,
|
||||||
|
& .--docs--main-editor .ProseMirror {
|
||||||
|
// Comments marks in the editor
|
||||||
|
.bn-editor {
|
||||||
|
.bn-thread-mark:not([data-orphan='true']),
|
||||||
|
.bn-thread-mark-selected:not([data-orphan='true']) {
|
||||||
|
background: ${canSeeComment ? '#EDB40066' : 'transparent'};
|
||||||
|
color: var(--c--theme--colors--greyscale-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
em-emoji-picker {
|
||||||
|
box-shadow: 0px 6px 18px 0px #00001229;
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread modal
|
||||||
|
.bn-thread {
|
||||||
|
width: 400px;
|
||||||
|
padding: 8px;
|
||||||
|
box-shadow: 0px 6px 18px 0px #00001229;
|
||||||
|
margin-left: 20px;
|
||||||
|
gap: 0;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 500px;
|
||||||
|
|
||||||
|
.bn-default-styles {
|
||||||
|
font-family: var(--c--theme--font--families--base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-block {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tooltip
|
||||||
|
*[role='tooltip'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-thread-comment {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
& .bn-editor {
|
||||||
|
padding-left: 32px;
|
||||||
|
.bn-inline-content {
|
||||||
|
color: var(--c--theme--colors--greyscale-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emoji
|
||||||
|
& .bn-badge-group {
|
||||||
|
padding-left: 32px;
|
||||||
|
.bn-badge label {
|
||||||
|
padding: 0 4px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--c--theme--colors--greyscale-300);
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top bar (Name / Date / Actions) when actions displayed
|
||||||
|
&:has(.bn-comment-actions) {
|
||||||
|
& > .mantine-Group-root {
|
||||||
|
max-width: 70%;
|
||||||
|
right: 0.3rem !important;
|
||||||
|
top: 0.3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-menu-dropdown {
|
||||||
|
box-shadow: 0px 0px 6px 0px #0000911a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top bar (Name / Date / Actions)
|
||||||
|
& > .mantine-Group-root {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
// Date
|
||||||
|
span.mantine-focus-auto {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-comment-actions {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.mantine-Button-root {
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--c--theme--colors--greyscale-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button[role='menuitem'] svg {
|
||||||
|
color: var(--c--theme--colors--greyscale-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
color: var(--c--theme--colors--info-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions button edit comment
|
||||||
|
.bn-container + .bn-comment-actions-wrapper {
|
||||||
|
.bn-comment-actions {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
gap: 0.4rem !important;
|
||||||
|
|
||||||
|
& > button {
|
||||||
|
height: 24px;
|
||||||
|
padding-inline: 4px;
|
||||||
|
|
||||||
|
&[data-test='save'] {
|
||||||
|
border: 1px solid var(--c--theme--colors--info-600);
|
||||||
|
background: var(--c--theme--colors--info-600);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-test='cancel'] {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid var(--c--theme--colors--greyscale-300);
|
||||||
|
color: var(--c--theme--colors--info-600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input to add a new comment
|
||||||
|
.bn-thread-composer,
|
||||||
|
&:has(> .bn-comment-editor + .bn-comment-actions-wrapper) {
|
||||||
|
padding: 0.5rem 8px;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.bn-container.bn-comment-editor {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
flex: 0 0 26px;
|
||||||
|
background-image: ${currentUserAvatarUrl
|
||||||
|
? `url("${currentUserAvatarUrl}")`
|
||||||
|
: 'none'};
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions button send comment
|
||||||
|
.bn-thread-composer .bn-comment-actions-wrapper,
|
||||||
|
&:not(.selected) .bn-comment-actions-wrapper {
|
||||||
|
flex-basis: fit-content;
|
||||||
|
|
||||||
|
.bn-action-toolbar.bn-comment-actions {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 0;
|
||||||
|
background: var(--c--theme--colors--info-600);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--c--theme--colors--greyscale-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .mantine-Button-label::before {
|
||||||
|
content: '🡡';
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--c--theme--colors--greyscale-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input first comment
|
||||||
|
&:not(.selected) {
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.bn-container.bn-comment-editor {
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.ProseMirror.bn-editor {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { CommentData, ThreadData } from '@blocknote/core/comments';
|
||||||
|
|
||||||
|
import { UserLight } from '@/features/auth';
|
||||||
|
|
||||||
|
export interface CommentAbilities {
|
||||||
|
destroy: boolean;
|
||||||
|
update: boolean;
|
||||||
|
partial_update: boolean;
|
||||||
|
retrieve: boolean;
|
||||||
|
reactions: boolean;
|
||||||
|
}
|
||||||
|
export interface ThreadAbilities {
|
||||||
|
destroy: boolean;
|
||||||
|
update: boolean;
|
||||||
|
partial_update: boolean;
|
||||||
|
retrieve: boolean;
|
||||||
|
resolve: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerReaction {
|
||||||
|
emoji: string;
|
||||||
|
created_at: string;
|
||||||
|
users: UserLight[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerComment {
|
||||||
|
id: string;
|
||||||
|
user: UserLight | null;
|
||||||
|
body: unknown;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
reactions: ServerReaction[];
|
||||||
|
abilities: CommentAbilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerThread {
|
||||||
|
id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
user: UserLight | null;
|
||||||
|
resolved: boolean;
|
||||||
|
resolved_updated_at: string | null;
|
||||||
|
resolved_by: string | null;
|
||||||
|
metadata: unknown;
|
||||||
|
comments: ServerComment[];
|
||||||
|
abilities: ThreadAbilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientCommentData = Omit<CommentData, 'metadata'> & {
|
||||||
|
metadata: { abilities: CommentAbilities };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClientThreadData = Omit<ThreadData, 'metadata'> & {
|
||||||
|
metadata: { abilities: ThreadAbilities; metadata: unknown };
|
||||||
|
};
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { User } from '@/features/auth';
|
||||||
|
import { Doc, useProviderStore } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
import { DocsThreadStore } from './DocsThreadStore';
|
||||||
|
import { DocsThreadStoreAuth } from './DocsThreadStoreAuth';
|
||||||
|
|
||||||
|
export function useComments(
|
||||||
|
docId: Doc['id'],
|
||||||
|
canComment: boolean,
|
||||||
|
user: User | null | undefined,
|
||||||
|
) {
|
||||||
|
const { provider } = useProviderStore();
|
||||||
|
const threadStore = useMemo(() => {
|
||||||
|
return new DocsThreadStore(
|
||||||
|
docId,
|
||||||
|
provider?.awareness ?? undefined,
|
||||||
|
new DocsThreadStoreAuth(
|
||||||
|
encodeURIComponent(user?.full_name || ''),
|
||||||
|
canComment,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [docId, canComment, provider?.awareness, user?.full_name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
threadStore?.destroy();
|
||||||
|
};
|
||||||
|
}, [threadStore]);
|
||||||
|
|
||||||
|
return threadStore;
|
||||||
|
}
|
||||||
-1
@@ -173,5 +173,4 @@ export const getCalloutFormattingToolbarItems = (
|
|||||||
name: t('Callout'),
|
name: t('Callout'),
|
||||||
type: 'callout',
|
type: 'callout',
|
||||||
icon: () => <Icon iconName="lightbulb" $size="16px" />,
|
icon: () => <Icon iconName="lightbulb" $size="16px" />,
|
||||||
isSelected: (block) => block.type === 'callout',
|
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-3
@@ -22,8 +22,8 @@ import { Box, Icon } from '@/components';
|
|||||||
import { DocsBlockNoteEditor } from '../../types';
|
import { DocsBlockNoteEditor } from '../../types';
|
||||||
|
|
||||||
const PDFBlockStyle = createGlobalStyle`
|
const PDFBlockStyle = createGlobalStyle`
|
||||||
.bn-block-content[data-content-type="pdf"] {
|
.bn-block-content[data-content-type="pdf"] .bn-file-block-content-wrapper[style*="fit-content"] {
|
||||||
width: fit-content;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ type CreatePDFBlockConfig = BlockConfig<
|
|||||||
|
|
||||||
interface PdfBlockComponentProps {
|
interface PdfBlockComponentProps {
|
||||||
block: BlockNoDefaults<
|
block: BlockNoDefaults<
|
||||||
Record<'callout', CreatePDFBlockConfig>,
|
Record<'pdf', CreatePDFBlockConfig>,
|
||||||
InlineContentSchema,
|
InlineContentSchema,
|
||||||
StyleSchema
|
StyleSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
+127
-15
@@ -1,34 +1,146 @@
|
|||||||
|
import {
|
||||||
|
BlockConfig,
|
||||||
|
BlockNoDefaults,
|
||||||
|
BlockNoteEditor,
|
||||||
|
InlineContentSchema,
|
||||||
|
StyleSchema,
|
||||||
|
} from '@blocknote/core';
|
||||||
import { createReactBlockSpec } from '@blocknote/react';
|
import { createReactBlockSpec } from '@blocknote/react';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, Text } from '@/components';
|
||||||
|
import { useMediaUrl } from '@/core';
|
||||||
|
|
||||||
|
import { loopCheckDocMediaStatus } from '../../api';
|
||||||
import Loader from '../../assets/loader.svg';
|
import Loader from '../../assets/loader.svg';
|
||||||
import Warning from '../../assets/warning.svg';
|
import Warning from '../../assets/warning.svg';
|
||||||
|
|
||||||
|
type UploadLoaderPropSchema = {
|
||||||
|
readonly information: { readonly default: '' };
|
||||||
|
readonly type: {
|
||||||
|
readonly default: 'loading';
|
||||||
|
readonly values: readonly ['loading', 'warning'];
|
||||||
|
};
|
||||||
|
readonly blockUploadName: { readonly default: '' };
|
||||||
|
readonly blockUploadShowPreview: { readonly default: true };
|
||||||
|
readonly blockUploadType: {
|
||||||
|
readonly default: '';
|
||||||
|
};
|
||||||
|
readonly blockUploadUrl: { readonly default: '' };
|
||||||
|
};
|
||||||
|
|
||||||
|
type UploadLoaderBlockConfig = BlockConfig<
|
||||||
|
'uploadLoader',
|
||||||
|
UploadLoaderPropSchema,
|
||||||
|
'none'
|
||||||
|
>;
|
||||||
|
|
||||||
|
type UploadLoaderBlockType = BlockNoDefaults<
|
||||||
|
Record<'uploadLoader', UploadLoaderBlockConfig>,
|
||||||
|
InlineContentSchema,
|
||||||
|
StyleSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
type UploadLoaderEditor = BlockNoteEditor<
|
||||||
|
Record<'uploadLoader', UploadLoaderBlockConfig>,
|
||||||
|
InlineContentSchema,
|
||||||
|
StyleSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface UploadLoaderBlockComponentProps {
|
||||||
|
block: UploadLoaderBlockType;
|
||||||
|
editor: UploadLoaderEditor;
|
||||||
|
contentRef: (node: HTMLElement | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UploadLoaderBlockComponent = ({
|
||||||
|
block,
|
||||||
|
editor,
|
||||||
|
}: UploadLoaderBlockComponentProps) => {
|
||||||
|
const mediaUrl = useMediaUrl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!block.props.blockUploadUrl || block.props.type !== 'loading') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = block.props.blockUploadUrl;
|
||||||
|
|
||||||
|
loopCheckDocMediaStatus(url)
|
||||||
|
.then((response) => {
|
||||||
|
// Replace the loading block with the resource block (image, audio, video, pdf ...)
|
||||||
|
try {
|
||||||
|
editor.replaceBlocks(
|
||||||
|
[block.id],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: block.props.blockUploadType,
|
||||||
|
props: {
|
||||||
|
url: `${mediaUrl}${response.file}`,
|
||||||
|
showPreview: block.props.blockUploadShowPreview,
|
||||||
|
name: block.props.blockUploadName,
|
||||||
|
caption: '',
|
||||||
|
backgroundColor: 'default',
|
||||||
|
textAlignment: 'left',
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* During collaboration, another user might have updated the block */
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error analyzing file:', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
editor.updateBlock(block.id, {
|
||||||
|
type: 'uploadLoader',
|
||||||
|
props: {
|
||||||
|
type: 'warning',
|
||||||
|
information: t(
|
||||||
|
'The antivirus has detected an anomaly in your file.',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* During collaboration, another user might have updated the block */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [block, editor, mediaUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="bn-visual-media-wrapper" $direction="row" $gap="0.5rem">
|
||||||
|
{block.props.type === 'warning' ? (
|
||||||
|
<Warning />
|
||||||
|
) : (
|
||||||
|
<Loader style={{ animation: 'spin 1.5s linear infinite' }} />
|
||||||
|
)}
|
||||||
|
<Text>{block.props.information}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const UploadLoaderBlock = createReactBlockSpec(
|
export const UploadLoaderBlock = createReactBlockSpec(
|
||||||
{
|
{
|
||||||
type: 'uploadLoader',
|
type: 'uploadLoader',
|
||||||
propSchema: {
|
propSchema: {
|
||||||
information: { default: '' as const },
|
information: { default: '' },
|
||||||
type: {
|
type: {
|
||||||
default: 'loading' as const,
|
default: 'loading',
|
||||||
values: ['loading', 'warning'] as const,
|
values: ['loading', 'warning'],
|
||||||
},
|
},
|
||||||
|
blockUploadName: { default: '' },
|
||||||
|
blockUploadShowPreview: { default: true },
|
||||||
|
blockUploadType: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
blockUploadUrl: { default: '' },
|
||||||
},
|
},
|
||||||
content: 'none',
|
content: 'none',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
render: ({ block }) => {
|
render: (props) => <UploadLoaderBlockComponent {...props} />,
|
||||||
return (
|
|
||||||
<Box className="bn-visual-media-wrapper" $direction="row" $gap="0.5rem">
|
|
||||||
{block.props.type === 'warning' ? (
|
|
||||||
<Warning />
|
|
||||||
) : (
|
|
||||||
<Loader style={{ animation: 'spin 1.5s linear infinite' }} />
|
|
||||||
)}
|
|
||||||
<Text>{block.props.information}</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-2
@@ -29,6 +29,9 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
|||||||
render: ({ inlineContent, updateInlineContent }) => {
|
render: ({ inlineContent, updateInlineContent }) => {
|
||||||
const { data: doc } = useDoc({ id: inlineContent.props.docId });
|
const { data: doc } = useDoc({ id: inlineContent.props.docId });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the content title if the referenced doc title changes
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (doc?.title && doc.title !== inlineContent.props.title) {
|
if (doc?.title && doc.title !== inlineContent.props.title) {
|
||||||
updateInlineContent({
|
updateInlineContent({
|
||||||
@@ -39,7 +42,15 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [inlineContent.props, doc?.title, updateInlineContent]);
|
|
||||||
|
/**
|
||||||
|
* ⚠️ When doing collaborative editing, doc?.title might be out of sync
|
||||||
|
* causing an infinite loop of updates.
|
||||||
|
* To prevent this, we only run this effect when doc?.title changes,
|
||||||
|
* not when inlineContent.props.title changes.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [doc?.title]);
|
||||||
|
|
||||||
return <LinkSelected {...inlineContent.props} />;
|
return <LinkSelected {...inlineContent.props} />;
|
||||||
},
|
},
|
||||||
@@ -66,7 +77,7 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
$css={css`
|
$css={css`
|
||||||
display: inline;
|
display: contents;
|
||||||
padding: 0.1rem 0.4rem;
|
padding: 0.1rem 0.4rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
& svg {
|
& svg {
|
||||||
@@ -78,6 +89,10 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
|||||||
background-color: ${colorsTokens['greyscale-100']};
|
background-color: ${colorsTokens['greyscale-100']};
|
||||||
}
|
}
|
||||||
transition: background-color 0.2s ease-in-out;
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
.--docs--doc-deleted & {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{emoji ? (
|
{emoji ? (
|
||||||
|
|||||||
+53
-44
@@ -3,6 +3,7 @@ import {
|
|||||||
StyleSchema,
|
StyleSchema,
|
||||||
} from '@blocknote/core';
|
} from '@blocknote/core';
|
||||||
import { useBlockNoteEditor } from '@blocknote/react';
|
import { useBlockNoteEditor } from '@blocknote/react';
|
||||||
|
import type { KeyboardEvent } from 'react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
@@ -99,6 +100,55 @@ export const SearchPage = ({
|
|||||||
}, 100);
|
}, 100);
|
||||||
}, [inputRef]);
|
}, [inputRef]);
|
||||||
|
|
||||||
|
const closeSearch = (insertContent: string) => {
|
||||||
|
updateInlineContent({
|
||||||
|
type: 'interlinkingSearchInline',
|
||||||
|
props: {
|
||||||
|
disabled: true,
|
||||||
|
trigger,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
contentRef(null);
|
||||||
|
editor.focus();
|
||||||
|
editor.insertInlineContent([insertContent]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
// Keep the trigger character ('@' or '/') in the editor when closing with Escape
|
||||||
|
closeSearch(trigger);
|
||||||
|
} else if (e.key === 'Backspace' && search.length === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeSearch('');
|
||||||
|
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
// Allow arrow keys to be handled by the command menu for navigation
|
||||||
|
const commandList = e.currentTarget
|
||||||
|
.closest('.inline-content')
|
||||||
|
?.nextElementSibling?.querySelector('[cmdk-list]');
|
||||||
|
|
||||||
|
// Create a synthetic keyboard event for the command menu
|
||||||
|
const syntheticEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: e.key,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
commandList?.dispatchEvent(syntheticEvent);
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
// Handle Enter key to select the currently highlighted item
|
||||||
|
const selectedItem = e.currentTarget
|
||||||
|
.closest('.inline-content')
|
||||||
|
?.nextElementSibling?.querySelector(
|
||||||
|
'[cmdk-item][data-selected="true"]',
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
selectedItem?.click();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="span" $position="relative">
|
<Box as="span" $position="relative">
|
||||||
<Box
|
<Box
|
||||||
@@ -124,50 +174,7 @@ export const SearchPage = ({
|
|||||||
const value = (e.target as HTMLInputElement).value;
|
const value = (e.target as HTMLInputElement).value;
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={handleKeyDown}
|
||||||
if (
|
|
||||||
(e.key === 'Backspace' && search.length === 0) ||
|
|
||||||
e.key === 'Escape'
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
updateInlineContent({
|
|
||||||
type: 'interlinkingSearchInline',
|
|
||||||
props: {
|
|
||||||
disabled: true,
|
|
||||||
trigger,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
contentRef(null);
|
|
||||||
editor.focus();
|
|
||||||
editor.insertInlineContent(['']);
|
|
||||||
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
||||||
// Allow arrow keys to be handled by the command menu for navigation
|
|
||||||
const commandList = e.currentTarget
|
|
||||||
.closest('.inline-content')
|
|
||||||
?.nextElementSibling?.querySelector('[cmdk-list]');
|
|
||||||
|
|
||||||
// Create a synthetic keyboard event for the command menu
|
|
||||||
const syntheticEvent = new KeyboardEvent('keydown', {
|
|
||||||
key: e.key,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
commandList?.dispatchEvent(syntheticEvent);
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
// Handle Enter key to select the currently highlighted item
|
|
||||||
const selectedItem = e.currentTarget
|
|
||||||
.closest('.inline-content')
|
|
||||||
?.nextElementSibling?.querySelector(
|
|
||||||
'[cmdk-item][data-selected="true"]',
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
selectedItem?.click();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
@@ -224,6 +231,8 @@ export const SearchPage = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
contentRef(null);
|
||||||
|
|
||||||
editor.insertInlineContent([
|
editor.insertInlineContent([
|
||||||
{
|
{
|
||||||
type: 'interlinkingLinkInline',
|
type: 'interlinkingLinkInline',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './BlockNoteEditor';
|
||||||
export * from './DocEditor';
|
export * from './DocEditor';
|
||||||
export * from './EmojiPicker';
|
export * from './EmojiPicker';
|
||||||
export * from './custom-blocks/';
|
export * from './custom-blocks/';
|
||||||
|
|||||||
+4
-35
@@ -43,7 +43,7 @@ describe('useSaveDoc', () => {
|
|||||||
|
|
||||||
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,37 +62,6 @@ describe('useSaveDoc', () => {
|
|||||||
addEventListenerSpy.mockRestore();
|
addEventListenerSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not save when canSave is false', () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const yDoc = new Y.Doc();
|
|
||||||
const docId = 'test-doc-id';
|
|
||||||
|
|
||||||
fetchMock.patch('http://test.jest/api/v1.0/documents/test-doc-id/', {
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: 'test-doc-id',
|
|
||||||
content: 'test-content',
|
|
||||||
title: 'test-title',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, false, true), {
|
|
||||||
wrapper: AppWrapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
// Trigger a local update
|
|
||||||
yDoc.getMap('test').set('key', 'value');
|
|
||||||
|
|
||||||
// Advance timers to trigger the save interval
|
|
||||||
vi.advanceTimersByTime(61000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Since canSave is false, no API call should be made
|
|
||||||
expect(fetchMock.calls().length).toBe(0);
|
|
||||||
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should save when there are local changes', async () => {
|
it('should save when there are local changes', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const yDoc = new Y.Doc();
|
const yDoc = new Y.Doc();
|
||||||
@@ -106,7 +75,7 @@ describe('useSaveDoc', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,7 +112,7 @@ describe('useSaveDoc', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +132,7 @@ describe('useSaveDoc', () => {
|
|||||||
const docId = 'test-doc-id';
|
const docId = 'test-doc-id';
|
||||||
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
||||||
|
|
||||||
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true, true), {
|
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true), {
|
||||||
wrapper: AppWrapper,
|
wrapper: AppWrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHeadings(editor);
|
setHeadings(editor);
|
||||||
|
|
||||||
editor?.onChange(() => {
|
const unsubscribe = editor?.onChange(() => {
|
||||||
setHeadings(editor);
|
setHeadings(editor);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resetHeadings();
|
resetHeadings();
|
||||||
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [editor, resetHeadings, setHeadings]);
|
}, [editor, resetHeadings, setHeadings]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ const SAVE_INTERVAL = 60000;
|
|||||||
export const useSaveDoc = (
|
export const useSaveDoc = (
|
||||||
docId: string,
|
docId: string,
|
||||||
yDoc: Y.Doc,
|
yDoc: Y.Doc,
|
||||||
canSave: boolean,
|
|
||||||
isConnectedToCollabServer: boolean,
|
isConnectedToCollabServer: boolean,
|
||||||
) => {
|
) => {
|
||||||
const { mutate: updateDoc } = useUpdateDoc({
|
const { mutate: updateDoc } = useUpdateDoc({
|
||||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsLocalChange(false);
|
setIsLocalChange(false);
|
||||||
},
|
},
|
||||||
@@ -47,7 +46,7 @@ export const useSaveDoc = (
|
|||||||
}, [yDoc]);
|
}, [yDoc]);
|
||||||
|
|
||||||
const saveDoc = useCallback(() => {
|
const saveDoc = useCallback(() => {
|
||||||
if (!canSave || !isLocalChange) {
|
if (!isLocalChange) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,14 +57,7 @@ export const useSaveDoc = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, [
|
}, [isLocalChange, updateDoc, docId, yDoc, isConnectedToCollabServer]);
|
||||||
canSave,
|
|
||||||
isLocalChange,
|
|
||||||
updateDoc,
|
|
||||||
docId,
|
|
||||||
yDoc,
|
|
||||||
isConnectedToCollabServer,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import { DocsBlockNoteEditor } from '../types';
|
import { DocsBlockNoteEditor } from '../types';
|
||||||
|
|
||||||
export const useShortcuts = (editor: DocsBlockNoteEditor) => {
|
export const useShortcuts = (
|
||||||
|
editor: DocsBlockNoteEditor,
|
||||||
|
el: HTMLDivElement | null,
|
||||||
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === '@' && editor?.isFocused()) {
|
if (event.key === '@' && editor?.isFocused()) {
|
||||||
@@ -29,11 +32,14 @@ export const useShortcuts = (editor: DocsBlockNoteEditor) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attach the event listener to the document instead of the window
|
if (!el) {
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
el.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [editor]);
|
}, [editor, el]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +1,12 @@
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { captureException } from '@sentry/nextjs';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { backendUrl } from '@/api';
|
import { backendUrl } from '@/api';
|
||||||
import { useMediaUrl } from '@/core/config';
|
|
||||||
import { sleep } from '@/utils';
|
|
||||||
|
|
||||||
import { checkDocMediaStatus, useCreateDocAttachment } from '../api';
|
import { useCreateDocAttachment } from '../api';
|
||||||
import { DocsBlockNoteEditor } from '../types';
|
import { DocsBlockNoteEditor } from '../types';
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload file can be analyzed on the server side,
|
|
||||||
* we had this function to wait for the analysis to be done
|
|
||||||
* before returning the file url. It will keep the loader
|
|
||||||
* on the upload button until the analysis is done.
|
|
||||||
* @param url
|
|
||||||
* @returns Promise<CheckDocMediaStatusResponse> status_code
|
|
||||||
* @description Waits for the upload to be analyzed by checking the status of the file.
|
|
||||||
*/
|
|
||||||
const loopCheckDocMediaStatus = async (url: string) => {
|
|
||||||
const SLEEP_TIME = 5000;
|
|
||||||
const response = await checkDocMediaStatus({
|
|
||||||
urlMedia: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 'ready') {
|
|
||||||
return response;
|
|
||||||
} else {
|
|
||||||
await sleep(SLEEP_TIME);
|
|
||||||
return await loopCheckDocMediaStatus(url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useUploadFile = (docId: string) => {
|
export const useUploadFile = (docId: string) => {
|
||||||
const {
|
const {
|
||||||
mutateAsync: createDocAttachment,
|
mutateAsync: createDocAttachment,
|
||||||
@@ -63,94 +39,9 @@ export const useUploadFile = (docId: string) => {
|
|||||||
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||||
const ANALYZE_URL = 'media-check';
|
const ANALYZE_URL = 'media-check';
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mediaUrl = useMediaUrl();
|
|
||||||
const timeoutIds = useRef<Record<string, NodeJS.Timeout>>({});
|
|
||||||
|
|
||||||
const blockAnalyzeProcess = useCallback(
|
|
||||||
(editor: DocsBlockNoteEditor, blockId: string, url: string) => {
|
|
||||||
if (timeoutIds.current[url]) {
|
|
||||||
clearTimeout(timeoutIds.current[url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay to let the time to the dom to be rendered
|
|
||||||
const timoutId = setTimeout(() => {
|
|
||||||
// Replace the resource block by a loading block
|
|
||||||
const { insertedBlocks, removedBlocks } = editor.replaceBlocks(
|
|
||||||
[blockId],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
type: 'uploadLoader',
|
|
||||||
props: {
|
|
||||||
information: t('Analyzing file...'),
|
|
||||||
type: 'loading',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
loopCheckDocMediaStatus(url)
|
|
||||||
.then((response) => {
|
|
||||||
if (insertedBlocks.length === 0 || removedBlocks.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingBlockId = insertedBlocks[0].id;
|
|
||||||
const removedBlock = removedBlocks[0];
|
|
||||||
|
|
||||||
removedBlock.props = {
|
|
||||||
...removedBlock.props,
|
|
||||||
url: `${mediaUrl}${response.file}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace the loading block with the resource block (image, audio, video, pdf ...)
|
|
||||||
editor.replaceBlocks([loadingBlockId], [removedBlock]);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error analyzing file:', error);
|
|
||||||
|
|
||||||
const loadingBlock = insertedBlocks[0];
|
|
||||||
|
|
||||||
if (!loadingBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingBlock.props = {
|
|
||||||
...loadingBlock.props,
|
|
||||||
type: 'warning',
|
|
||||||
information: t(
|
|
||||||
'The antivirus has detected an anomaly in your file.',
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.updateBlock(loadingBlock.id, loadingBlock);
|
|
||||||
});
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
timeoutIds.current[url] = timoutId;
|
|
||||||
},
|
|
||||||
[t, mediaUrl],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const blocksAnalyze = editor?.document.filter(
|
const unsubscribe = editor.onChange((_, context) => {
|
||||||
(block) => 'url' in block.props && block.props.url.includes(ANALYZE_URL),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!blocksAnalyze?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
blocksAnalyze.forEach((block) => {
|
|
||||||
if (!('url' in block.props)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockAnalyzeProcess(editor, block.id, block.props.url);
|
|
||||||
});
|
|
||||||
}, [blockAnalyzeProcess, editor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
editor.onChange((_, context) => {
|
|
||||||
const blocksChanges = context.getChanges();
|
const blocksChanges = context.getChanges();
|
||||||
|
|
||||||
if (!blocksChanges.length) {
|
if (!blocksChanges.length) {
|
||||||
@@ -169,11 +60,49 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockAnalyzeProcess(
|
const blockUploadUrl = blockChanges.block.props.url;
|
||||||
editor,
|
const blockUploadType = blockChanges.block.type;
|
||||||
blockChanges.block.id,
|
const blockUploadName = blockChanges.block.props.name;
|
||||||
blockChanges.block.props.url,
|
const blockUploadShowPreview =
|
||||||
);
|
('showPreview' in blockChanges.block.props &&
|
||||||
|
blockChanges.block.props.showPreview) ||
|
||||||
|
false;
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
// Replace the resource block by a uploadLoader block
|
||||||
|
// to show analyzing status
|
||||||
|
try {
|
||||||
|
editor.replaceBlocks(
|
||||||
|
[blockChanges.block.id],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'uploadLoader',
|
||||||
|
props: {
|
||||||
|
information: t('Analyzing file...'),
|
||||||
|
type: 'loading',
|
||||||
|
blockUploadName,
|
||||||
|
blockUploadType,
|
||||||
|
blockUploadUrl,
|
||||||
|
blockUploadShowPreview,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
captureException(error, {
|
||||||
|
extra: { info: 'Error replacing block for upload loader' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}, [blockAnalyzeProcess, mediaUrl, editor, t]);
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [editor, t]);
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user