Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9790824ce1 |
@@ -96,10 +96,10 @@ jobs:
|
||||
- name: Check linting
|
||||
run: cd src/frontend/ && yarn lint
|
||||
|
||||
test-e2e-chromium:
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-front
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -158,88 +158,14 @@ jobs:
|
||||
make demo FLUSH_ARGS='--no-input'
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: cd src/frontend/apps/e2e && yarn install-playwright chromium
|
||||
run: cd src/frontend/apps/e2e && yarn install
|
||||
|
||||
- name: Run e2e tests
|
||||
run: cd src/frontend/ && yarn e2e:test --project='chromium'
|
||||
run: cd src/frontend/ && yarn e2e:test
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-chromium-report
|
||||
path: src/frontend/apps/e2e/report/
|
||||
retention-days: 7
|
||||
|
||||
test-e2e-other-browser:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-front
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set services env variables
|
||||
run: |
|
||||
make data/media
|
||||
make create-env-files
|
||||
cat env.d/development/common.e2e.dist >> env.d/development/common
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
- name: Restore the build cache
|
||||
uses: actions/cache@v4
|
||||
id: cache-build
|
||||
with:
|
||||
path: src/frontend/apps/impress/out/
|
||||
key: build-front-${{ github.run_id }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build the Docker images
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
targets: |
|
||||
app-dev
|
||||
y-webrtc-signaling
|
||||
load: true
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=cached-stage
|
||||
*.cache-to=type=gha,scope=cached-stage,mode=max
|
||||
|
||||
- name: Start Docker services
|
||||
run: |
|
||||
make run
|
||||
|
||||
- name: Apply DRF migrations
|
||||
run: |
|
||||
make migrate
|
||||
|
||||
- name: Add dummy data
|
||||
run: |
|
||||
make demo FLUSH_ARGS='--no-input'
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: cd src/frontend/apps/e2e && yarn install-playwright firefox webkit chromium
|
||||
|
||||
- name: Run e2e tests
|
||||
run: cd src/frontend/ && yarn e2e:test --project=firefox --project=webkit
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-other-report
|
||||
name: playwright-report
|
||||
path: src/frontend/apps/e2e/report/
|
||||
retention-days: 7
|
||||
|
||||
@@ -198,10 +198,75 @@ jobs:
|
||||
- name: Install gettext (required to compile messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext pandoc
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: python manage.py compilemessages
|
||||
|
||||
- name: Run tests
|
||||
run: ~/.local/bin/pytest -n 2
|
||||
|
||||
i18n-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "infrastructure,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
|
||||
- name: Install gettext (required to make messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install development dependencies
|
||||
working-directory: src/backend
|
||||
run: pip install --user .[dev]
|
||||
|
||||
- name: Generate the translation base file
|
||||
run: ~/.local/bin/django-admin makemessages --keep-pot --all
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.x"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: src/frontend/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && yarn install --frozen-lockfile
|
||||
|
||||
- name: Extract the frontend translation
|
||||
run: make frontend-i18n-extract
|
||||
|
||||
- name: Upload files to Crowdin
|
||||
run: |
|
||||
docker run \
|
||||
--rm \
|
||||
-e CROWDIN_API_TOKEN=$CROWDIN_API_TOKEN \
|
||||
-e CROWDIN_PROJECT_ID=$CROWDIN_PROJECT_ID \
|
||||
-e CROWDIN_BASE_PATH=$CROWDIN_BASE_PATH \
|
||||
-v "${{ github.workspace }}:/app" \
|
||||
crowdin/cli:3.16.0 \
|
||||
crowdin upload sources -c /app/crowdin/config.yml
|
||||
|
||||
@@ -33,6 +33,7 @@ MANIFEST
|
||||
*.pot
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
@@ -49,6 +50,9 @@ node_modules
|
||||
# Mails
|
||||
src/backend/core/templates/mail/
|
||||
|
||||
# Typescript client
|
||||
src/frontend/tsclient
|
||||
|
||||
# Swagger
|
||||
**/swagger.json
|
||||
|
||||
|
||||
@@ -6,5 +6,4 @@ creation_rules:
|
||||
- age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7 # github-repo
|
||||
- age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg # Anthony Le-Courric
|
||||
- age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3 # Antoine Lebaud
|
||||
- age1hnhuzj96ktkhpyygvmz0x9h8mfvssz7ss6emmukags644mdhf4msajk93r # Samuel Paccoud
|
||||
|
||||
|
||||
@@ -8,41 +8,6 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [1.2.1] - 2024-08-23
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️ Change ordering docs datagrid #195
|
||||
- 🔥(helm) use scaleway email #194
|
||||
|
||||
|
||||
## [1.2.0] - 2024-08-22
|
||||
|
||||
## Added
|
||||
|
||||
- 🎨(frontend) better conversion editor to pdf #151
|
||||
- ✨Export docx (word) #161
|
||||
- 🌐Internationalize invitation email #167
|
||||
- ✨(frontend) White branding #164
|
||||
- ✨Email invitation when add user to doc #171
|
||||
- ✨Invitation management #174
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(y-webrtc) fix prob connection #147
|
||||
- ⚡️(frontend) improve select share stability #159
|
||||
- 🐛(backend) enable SSL when sending email #165
|
||||
|
||||
## Changed
|
||||
|
||||
- 🎨(frontend) stop limit layout height to screen size #158
|
||||
- ⚡️(CI) only e2e chrome mandatory #177
|
||||
|
||||
## Removed
|
||||
- 🔥(helm) remove htaccess #181
|
||||
|
||||
|
||||
## [1.1.0] - 2024-07-15
|
||||
|
||||
## Added
|
||||
@@ -58,7 +23,6 @@ and this project adheres to
|
||||
- ♻️(frontend) create a doc from a modal #132
|
||||
- ♻️(frontend) manage members from the share modal #140
|
||||
|
||||
|
||||
## [1.0.0] - 2024-07-02
|
||||
|
||||
## Added
|
||||
@@ -97,7 +61,6 @@ and this project adheres to
|
||||
- 💚(CI) Remove trigger workflow on push tags on CI (#68)
|
||||
- 🔥(frontend) Remove coming soon page (#121)
|
||||
|
||||
|
||||
## [0.1.0] - 2024-05-24
|
||||
|
||||
## Added
|
||||
@@ -106,9 +69,7 @@ and this project adheres to
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.2.1...main
|
||||
[1.2.1]: https://github.com/numerique-gouv/impress/releases/v1.2.1
|
||||
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v1.1.0...main
|
||||
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
|
||||
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
|
||||
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
|
||||
@@ -75,7 +75,6 @@ RUN apt-get update && \
|
||||
libgdk-pixbuf2.0-0 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
pandoc \
|
||||
shared-mime-info && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT
|
||||
# -- Backend
|
||||
MANAGE = $(COMPOSE_RUN_APP) python manage.py
|
||||
MAIL_YARN = $(COMPOSE_RUN) -w /app/src/mail node yarn
|
||||
TSCLIENT_YARN = $(COMPOSE_RUN) -w /app/src/tsclient node yarn
|
||||
|
||||
# -- Frontend
|
||||
PATH_FRONT = ./src/frontend
|
||||
@@ -185,7 +186,7 @@ back-i18n-compile: ## compile the gettext files
|
||||
.PHONY: back-i18n-compile
|
||||
|
||||
back-i18n-generate: ## create the .pot files used for i18n
|
||||
@$(MANAGE) makemessages -a --keep-pot --all
|
||||
@$(MANAGE) makemessages -a --keep-pot
|
||||
.PHONY: back-i18n-generate
|
||||
|
||||
shell: ## connect to database shell
|
||||
@@ -274,6 +275,16 @@ mails-install: ## install the mail generator
|
||||
@$(MAIL_YARN) install
|
||||
.PHONY: mails-install
|
||||
|
||||
# -- TS client generator
|
||||
|
||||
tsclient-install: ## Install the Typescript API client generator
|
||||
@$(TSCLIENT_YARN) install
|
||||
.PHONY: tsclient-install
|
||||
|
||||
tsclient: tsclient-install ## Generate a Typescript API client
|
||||
@$(TSCLIENT_YARN) generate:api:client:local ../frontend/tsclient
|
||||
.PHONY: tsclient-install
|
||||
|
||||
# -- Misc
|
||||
clean: ## restore repository state as it was freshly cloned
|
||||
git clean -idx
|
||||
@@ -313,13 +324,3 @@ start-tilt: ## start the kubernetes cluster using kind
|
||||
tilt up -f ./bin/Tiltfile
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
VERSION_TYPE ?= minor
|
||||
bump-packages-version: ## bump the version of the project - VERSION_TYPE can be "major", "minor", "patch"
|
||||
cd ./src/mail && yarn version --no-git-tag-version --$(VERSION_TYPE)
|
||||
cd ./src/frontend/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
|
||||
cd ./src/frontend/apps/e2e/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
|
||||
cd ./src/frontend/apps/impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
|
||||
cd ./src/frontend/apps/y-webrtc-signaling/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
|
||||
cd ./src/frontend/packages/eslint-config-impress/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
|
||||
cd ./src/frontend/packages/i18n/ && yarn version --no-git-tag-version --$(VERSION_TYPE)
|
||||
.PHONY: bump-packages-version
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# Releasing a new version
|
||||
|
||||
Whenever we are cooking a new release (e.g. `4.18.1`) we should follow a standard procedure described below:
|
||||
|
||||
1. Create a new branch named: `release/4.18.1`.
|
||||
2. Bump the release number for backend project, frontend projects, and Helm files:
|
||||
|
||||
- for backend, update the version number by hand in `pyproject.toml`,
|
||||
- for each projects (`src/frontend`, `src/frontend/apps/*`, `src/frontend/packages/*`, `src/mail`), run `yarn version --new-version --no-git-tag-version 4.18.1` in their directory. This will update their `package.json` for you,
|
||||
- for Helm, update Docker image tag in files located at `src/helm/env.d` for both `preprod` and `production` environments:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
repository: lasuite/impress-backend
|
||||
pullPolicy: Always
|
||||
tag: "v4.18.1" # Replace with your new version number, without forgetting the "v" prefix
|
||||
|
||||
...
|
||||
|
||||
frontend:
|
||||
image:
|
||||
repository: lasuite/impress-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v4.18.1"
|
||||
|
||||
webrtc:
|
||||
image:
|
||||
repository: lasuite/impress-y-webrtc-signaling
|
||||
pullPolicy: Always
|
||||
tag: "v4.18.1"
|
||||
```
|
||||
|
||||
The new images don't exist _yet_: they will be created automatically later in the process.
|
||||
|
||||
3. Update the project's `Changelog` following the [keepachangelog](https://keepachangelog.com/en/0.3.0/) recommendations
|
||||
|
||||
4. Commit your changes with the following format: the 🔖 release emoji, the type of release (patch/minor/patch) and the release version:
|
||||
|
||||
```text
|
||||
🔖(minor) bump release to 4.18.0
|
||||
```
|
||||
|
||||
5. Open a pull request, wait for an approval from your peers and merge it.
|
||||
6. Checkout and pull changes from the `main` branch to ensure you have the latest updates.
|
||||
7. Tag and push your commit:
|
||||
|
||||
```bash
|
||||
git tag v4.18.1 && git push origin tag v4.18.1
|
||||
```
|
||||
|
||||
Doing this triggers the CI and tells it to build the new Docker image versions that you targeted earlier in the Helm files.
|
||||
|
||||
8. Ensure the new [backend](https://hub.docker.com/r/lasuite/impress-frontend/tags) and [frontend](https://hub.docker.com/r/lasuite/impress-frontend/tags) image tags are on Docker Hub.
|
||||
9. The release is now done!
|
||||
|
||||
# Deploying
|
||||
|
||||
> [!TIP]
|
||||
> The `staging` platform is deployed automatically with every update of the `main` branch.
|
||||
|
||||
Making a new release doesn't publish it automatically in production.
|
||||
|
||||
Deployment is done by ArgoCD. ArgoCD checks for the `production` tag and automatically deploys the production platform with the targeted commit.
|
||||
|
||||
To publish, we mark the commit we want with the `production` tag. ArgoCD is then notified that the tag has changed. It then deploys the Docker image tags specified in the Helm files of the targeted commit.
|
||||
|
||||
To publish the release you just made:
|
||||
|
||||
```bash
|
||||
git tag --force production v4.18.1
|
||||
git push --force origin production
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# Api client TypeScript
|
||||
|
||||
The backend application can automatically create a TypeScript client to be used in frontend
|
||||
applications. It is used in the impress front application itself.
|
||||
|
||||
This client is made with [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen)
|
||||
and impress's backend OpenAPI schema (available [here](http://localhost:8071/v1.0/swagger/) if you have the backend running).
|
||||
|
||||
## Requirements
|
||||
|
||||
We'll need the online OpenAPI schema generated by swagger. Therefore you will first need to
|
||||
install the backend application.
|
||||
|
||||
## Install openApiClientJs
|
||||
|
||||
```sh
|
||||
$ cd src/tsclient
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
## Generate the client
|
||||
|
||||
```sh
|
||||
yarn generate:api:client:local <output_path_for_generated_client>
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name "*.enc.*" -exec sops updatekeys -y {} \;
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Admin classes and registrations for core app."""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Impress core API endpoints"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -17,9 +16,9 @@ def exception_handler(exc, context):
|
||||
https://gist.github.com/twidi/9d55486c36b6a51bdcb05ce3a763e79f
|
||||
"""
|
||||
if isinstance(exc, ValidationError):
|
||||
detail = exc.message_dict
|
||||
|
||||
if hasattr(exc, "message"):
|
||||
if hasattr(exc, "message_dict"):
|
||||
detail = exc.message_dict
|
||||
elif hasattr(exc, "message"):
|
||||
detail = exc.message
|
||||
elif hasattr(exc, "messages"):
|
||||
detail = exc.messages
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""A JSONField for DRF to handle serialization/deserialization."""
|
||||
|
||||
import json
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Permission handlers for the impress core app."""
|
||||
|
||||
from django.core import exceptions
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Client serializers for the impress core app."""
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -181,12 +180,6 @@ class DocumentGenerationSerializer(serializers.Serializer):
|
||||
required=False,
|
||||
default="html",
|
||||
)
|
||||
format = serializers.ChoiceField(
|
||||
choices=["pdf", "docx"],
|
||||
label=_("Format"),
|
||||
required=False,
|
||||
default="pdf",
|
||||
)
|
||||
|
||||
|
||||
class InvitationSerializer(serializers.ModelSerializer):
|
||||
@@ -266,12 +259,3 @@ class InvitationSerializer(serializers.ModelSerializer):
|
||||
attrs["document_id"] = document_id
|
||||
attrs["issuer"] = user
|
||||
return attrs
|
||||
|
||||
|
||||
class DocumentVersionSerializer(serializers.Serializer):
|
||||
"""Serialize Versions."""
|
||||
|
||||
etag = serializers.CharField()
|
||||
is_latest = serializers.BooleanField()
|
||||
last_modified = serializers.DateTimeField()
|
||||
version_id = serializers.CharField()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""API endpoints"""
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.db.models import (
|
||||
@@ -6,7 +7,7 @@ from django.db.models import (
|
||||
Q,
|
||||
Subquery,
|
||||
)
|
||||
from django.http import Http404
|
||||
from django.http import FileResponse, Http404
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
from rest_framework import (
|
||||
@@ -23,7 +24,6 @@ from rest_framework import (
|
||||
)
|
||||
|
||||
from core import models
|
||||
from core.utils import email_invitation
|
||||
|
||||
from . import permissions, serializers
|
||||
|
||||
@@ -310,7 +310,6 @@ class DocumentViewSet(
|
||||
access_model_class = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
queryset = models.Document.objects.all()
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
@@ -338,18 +337,10 @@ class DocumentViewSet(
|
||||
Q(user=request.user) | Q(team__in=request.user.get_teams()),
|
||||
)
|
||||
)
|
||||
|
||||
versions_data = document.get_versions_slice(from_datetime=from_datetime)[
|
||||
"versions"
|
||||
]
|
||||
paginator = pagination.PageNumberPagination()
|
||||
paginated_versions = paginator.paginate_queryset(versions_data, request)
|
||||
serialized_versions = serializers.DocumentVersionSerializer(
|
||||
paginated_versions, many=True
|
||||
return drf_response.Response(
|
||||
document.get_versions_slice(from_datetime=from_datetime)
|
||||
)
|
||||
|
||||
return paginator.get_paginated_response(serialized_versions.data)
|
||||
|
||||
@decorators.action(
|
||||
detail=True,
|
||||
methods=["get", "delete"],
|
||||
@@ -386,7 +377,6 @@ class DocumentViewSet(
|
||||
{
|
||||
"content": response["Body"].read().decode("utf-8"),
|
||||
"last_modified": response["LastModified"],
|
||||
"id": version_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -431,13 +421,6 @@ class DocumentAccessViewSet(
|
||||
resource_field_name = "document"
|
||||
serializer_class = serializers.DocumentAccessSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Add a new access to the document and send an email to the new added user."""
|
||||
access = serializer.save()
|
||||
|
||||
language = self.request.headers.get("Content-Language", "en-us")
|
||||
email_invitation(language, access.user.email, access.document.id)
|
||||
|
||||
|
||||
class TemplateViewSet(
|
||||
ResourceViewsetMixin,
|
||||
@@ -468,16 +451,7 @@ class TemplateViewSet(
|
||||
# pylint: disable=unused-argument
|
||||
def generate_document(self, request, pk=None):
|
||||
"""
|
||||
Generate and return a document for this template around the
|
||||
body passed as argument.
|
||||
|
||||
2 types of body are accepted:
|
||||
- HTML: body_type = "html"
|
||||
- Markdown: body_type = "markdown"
|
||||
|
||||
2 types of documents can be generated:
|
||||
- PDF: format = "pdf"
|
||||
- Docx: format = "docx"
|
||||
Generate and return pdf for this template with the content passed.
|
||||
"""
|
||||
serializer = serializers.DocumentGenerationSerializer(data=request.data)
|
||||
|
||||
@@ -488,10 +462,13 @@ class TemplateViewSet(
|
||||
|
||||
body = serializer.validated_data["body"]
|
||||
body_type = serializer.validated_data["body_type"]
|
||||
export_format = serializer.validated_data["format"]
|
||||
|
||||
template = self.get_object()
|
||||
return template.generate_document(body, body_type, export_format)
|
||||
pdf_content = template.generate_document(body, body_type)
|
||||
|
||||
response = FileResponse(BytesIO(pdf_content), content_type="application/pdf")
|
||||
response["Content-Disposition"] = f"attachment; filename={template.title}.pdf"
|
||||
return response
|
||||
|
||||
|
||||
class TemplateAccessViewSet(
|
||||
@@ -540,7 +517,6 @@ class InvitationViewset(
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""API ViewSet for user invitations to document.
|
||||
@@ -554,9 +530,8 @@ class InvitationViewset(
|
||||
- role: str [administrator|editor|reader]
|
||||
Return newly created invitation (issuer and document are automatically set)
|
||||
|
||||
PATCH /api/v1.0/documents/<document_id>/invitations/:<invitation_id>/ with expected data:
|
||||
- role: str [owner|admin|editor|reader]
|
||||
Return partially updated document invitation
|
||||
PUT / PATCH : Not permitted. Instead of updating your invitation,
|
||||
delete and create a new one.
|
||||
|
||||
DELETE /api/v1.0/documents/<document_id>/invitations/<invitation_id>/
|
||||
Delete targeted invitation
|
||||
@@ -610,10 +585,3 @@ class InvitationViewset(
|
||||
.distinct()
|
||||
)
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Save invitation to a document then send an email to the invited user."""
|
||||
invitation = serializer.save()
|
||||
|
||||
language = self.request.headers.get("Content-Language", "en-us")
|
||||
email_invitation(language, invitation.email, invitation.document.id)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Core application enums declaration
|
||||
"""
|
||||
|
||||
from django.conf import global_settings, settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"""
|
||||
Core application factories
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
"""
|
||||
Declare and configure the models for the impress core application
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import tempfile
|
||||
import smtplib
|
||||
import textwrap
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import models as auth_models
|
||||
from django.contrib.auth.base_user import AbstractBaseUser
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import exceptions, mail, validators
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import models
|
||||
from django.http import FileResponse
|
||||
from django.template.base import Template as DjangoTemplate
|
||||
from django.template.context import Context
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import html, timezone
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import override
|
||||
|
||||
import frontmatter
|
||||
import markdown
|
||||
import pypandoc
|
||||
import weasyprint
|
||||
from botocore.exceptions import ClientError
|
||||
from timezone_field import TimeZoneField
|
||||
from weasyprint import CSS, HTML
|
||||
from weasyprint.text.fonts import FontConfiguration
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@@ -316,29 +316,6 @@ class Document(BaseModel):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Write content to object storage only if _content has changed."""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self._content:
|
||||
file_key = self.file_key
|
||||
bytes_content = self._content.encode("utf-8")
|
||||
|
||||
if default_storage.exists(file_key):
|
||||
response = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=file_key
|
||||
)
|
||||
has_changed = (
|
||||
response["ETag"].strip('"')
|
||||
!= hashlib.md5(bytes_content).hexdigest() # noqa
|
||||
)
|
||||
else:
|
||||
has_changed = True
|
||||
|
||||
if has_changed:
|
||||
content_file = ContentFile(bytes_content)
|
||||
default_storage.save(file_key, content_file)
|
||||
|
||||
@property
|
||||
def key_base(self):
|
||||
"""Key base of the location where the document is stored in object storage."""
|
||||
@@ -379,6 +356,28 @@ class Document(BaseModel):
|
||||
Bucket=default_storage.bucket_name, Key=self.file_key, VersionId=version_id
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Write content to object storage only if _content has changed."""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if self._content:
|
||||
file_key = self.file_key
|
||||
bytes_content = self._content.encode("utf-8")
|
||||
|
||||
if default_storage.exists(file_key):
|
||||
response = default_storage.connection.meta.client.head_object(
|
||||
Bucket=default_storage.bucket_name, Key=file_key
|
||||
)
|
||||
has_changed = (
|
||||
response["ETag"].strip('"')
|
||||
!= hashlib.md5(bytes_content).hexdigest() # noqa
|
||||
)
|
||||
else:
|
||||
has_changed = True
|
||||
if has_changed:
|
||||
content_file = ContentFile(bytes_content)
|
||||
default_storage.save(file_key, content_file)
|
||||
|
||||
def get_versions_slice(
|
||||
self, from_version_id="", from_datetime=None, page_size=None
|
||||
):
|
||||
@@ -565,90 +564,10 @@ class Template(BaseModel):
|
||||
"retrieve": can_get,
|
||||
}
|
||||
|
||||
def generate_pdf(self, body_html, metadata):
|
||||
def generate_document(self, body, body_type):
|
||||
"""
|
||||
Generate and return a pdf document wrapped around the current template
|
||||
"""
|
||||
document_html = weasyprint.HTML(
|
||||
string=DjangoTemplate(self.code).render(
|
||||
Context({"body": html.format_html(body_html), **metadata})
|
||||
)
|
||||
)
|
||||
css = weasyprint.CSS(
|
||||
string=self.css,
|
||||
font_config=weasyprint.text.fonts.FontConfiguration(),
|
||||
)
|
||||
|
||||
pdf_content = document_html.write_pdf(stylesheets=[css], zoom=1)
|
||||
response = FileResponse(BytesIO(pdf_content), content_type="application/pdf")
|
||||
response["Content-Disposition"] = f"attachment; filename={self.title}.pdf"
|
||||
|
||||
return response
|
||||
|
||||
def generate_word(self, body_html, metadata):
|
||||
"""
|
||||
Generate and return a docx document wrapped around the current template
|
||||
"""
|
||||
template_string = DjangoTemplate(self.code).render(
|
||||
Context({"body": html.format_html(body_html), **metadata})
|
||||
)
|
||||
|
||||
html_string = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
{self.css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{template_string}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
reference_docx = "core/static/reference.docx"
|
||||
output = BytesIO()
|
||||
|
||||
# Convert the HTML to a temporary docx file
|
||||
with tempfile.NamedTemporaryFile(suffix=".docx", prefix="docx_") as tmp_file:
|
||||
output_path = tmp_file.name
|
||||
|
||||
pypandoc.convert_text(
|
||||
html_string,
|
||||
"docx",
|
||||
format="html",
|
||||
outputfile=output_path,
|
||||
extra_args=["--reference-doc", reference_docx],
|
||||
)
|
||||
|
||||
# Create a BytesIO object to store the output of the temporary docx file
|
||||
with open(output_path, "rb") as f:
|
||||
output = BytesIO(f.read())
|
||||
|
||||
# Ensure the pointer is at the beginning
|
||||
output.seek(0)
|
||||
|
||||
response = FileResponse(
|
||||
output,
|
||||
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
)
|
||||
response["Content-Disposition"] = f"attachment; filename={self.title}.docx"
|
||||
|
||||
return response
|
||||
|
||||
def generate_document(self, body, body_type, export_format):
|
||||
"""
|
||||
Generate and return a document for this template around the
|
||||
Generate and return a PDF document for this template around the
|
||||
body passed as argument.
|
||||
|
||||
2 types of body are accepted:
|
||||
- HTML: body_type = "html"
|
||||
- Markdown: body_type = "markdown"
|
||||
|
||||
2 types of documents can be generated:
|
||||
- PDF: export_format = "pdf"
|
||||
- Docx: export_format = "docx"
|
||||
"""
|
||||
document = frontmatter.loads(body)
|
||||
metadata = document.metadata
|
||||
@@ -661,10 +580,16 @@ class Template(BaseModel):
|
||||
markdown.markdown(textwrap.dedent(strip_body)) if strip_body else ""
|
||||
)
|
||||
|
||||
if export_format == "pdf":
|
||||
return self.generate_pdf(body_html, metadata)
|
||||
|
||||
return self.generate_word(body_html, metadata)
|
||||
document_html = HTML(
|
||||
string=DjangoTemplate(self.code).render(
|
||||
Context({"body": html.format_html(body_html), **metadata})
|
||||
)
|
||||
)
|
||||
css = CSS(
|
||||
string=self.css,
|
||||
font_config=FontConfiguration(),
|
||||
)
|
||||
return document_html.write_pdf(stylesheets=[css], zoom=1)
|
||||
|
||||
|
||||
class TemplateAccess(BaseAccess):
|
||||
@@ -743,6 +668,14 @@ class Invitation(BaseModel):
|
||||
def __str__(self):
|
||||
return f"{self.email} invited to {self.document}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Make invitations read-only."""
|
||||
if self.created_at:
|
||||
raise exceptions.PermissionDenied()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
self.email_invitation()
|
||||
|
||||
def clean(self):
|
||||
"""Validate fields."""
|
||||
super().clean()
|
||||
@@ -765,7 +698,6 @@ class Invitation(BaseModel):
|
||||
def get_abilities(self, user):
|
||||
"""Compute and return abilities for a given user."""
|
||||
can_delete = False
|
||||
can_update = False
|
||||
roles = []
|
||||
|
||||
if user.is_authenticated:
|
||||
@@ -784,13 +716,29 @@ class Invitation(BaseModel):
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
|
||||
can_update = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": can_update,
|
||||
"partial_update": can_update,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"retrieve": bool(roles),
|
||||
}
|
||||
|
||||
def email_invitation(self):
|
||||
"""Email invitation to the user."""
|
||||
try:
|
||||
with override(self.issuer.language):
|
||||
title = _("Invitation to join Impress!")
|
||||
template_vars = {"title": title, "site": Site.objects.get_current()}
|
||||
msg_html = render_to_string("mail/html/invitation.html", template_vars)
|
||||
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
|
||||
mail.send_mail(
|
||||
title,
|
||||
msg_plain,
|
||||
settings.EMAIL_FROM,
|
||||
[self.email],
|
||||
html_message=msg_html,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
except smtplib.SMTPException as exception:
|
||||
logger.error("invitation to %s was not sent: %s", self.email, exception)
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -92,12 +92,9 @@ def test_models_oidc_user_getter_invalid_token(django_assert_num_queries, monkey
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with (
|
||||
django_assert_num_queries(0),
|
||||
pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match="User info contained no recognizable user identification",
|
||||
),
|
||||
with django_assert_num_queries(0), pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match="User info contained no recognizable user identification",
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Fixtures for tests in the impress core application"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
"""
|
||||
Test document accesses API endpoints for users in impress's core app.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from django.core import mail
|
||||
|
||||
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_document_accesses_create_anonymous():
|
||||
"""Anonymous users should not be allowed to create document accesses."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(user.id),
|
||||
"document": str(document.id),
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
assert models.DocumentAccess.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_document_accesses_create_authenticated_unrelated():
|
||||
"""
|
||||
Authenticated users should not be allowed to create document accesses for a document to
|
||||
which they are not related.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_reader_or_editor(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Readers or editors of a document should not be allowed to create document accesses."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, 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/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": new_role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_administrator(
|
||||
via, mock_user_get_teams
|
||||
):
|
||||
"""
|
||||
Administrators of a document should be able to create document accesses
|
||||
except for the "owner" role.
|
||||
An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="administrator"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, 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/documents/{document.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 resource can assign other users as owners."
|
||||
}
|
||||
|
||||
# It should be allowed to create a lower access
|
||||
role = random.choice(
|
||||
[role[0] for role in models.RoleChoices.choices if role[0] != "owner"]
|
||||
)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user_id": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"id": str(new_document_access.id),
|
||||
"team": "",
|
||||
"role": role,
|
||||
"user": other_user,
|
||||
}
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [other_user["email"]]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner(via, mock_user_get_teams):
|
||||
"""
|
||||
Owners of a document should be able to create document accesses whatever the role.
|
||||
An email should be sent to the accesses to notify them of the adding.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user_id": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user,
|
||||
"team": "",
|
||||
"role": role,
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
}
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == [other_user["email"]]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert "docs/" + str(document.id) + "/" in email_content
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: create
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: delete
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: list
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@@ -169,26 +168,87 @@ def test_api_documents_list_authenticated_distinct():
|
||||
assert content["results"][0]["id"] == str(document.id)
|
||||
|
||||
|
||||
def test_api_documents_order_updated_at_desc_default():
|
||||
def test_api_documents_order_created_at_desc():
|
||||
"""
|
||||
Test that the endpoint GET documents is sorted in 'updated_at' descending order by default.
|
||||
Test that the endpoint GET documents is sorted in 'created_at' descending order by default.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents_created = [
|
||||
document.created_at.isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
|
||||
documents_created.sort(reverse=True)
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
response_document_created = [
|
||||
document["created_at"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
assert (
|
||||
response_document_created == documents_created
|
||||
), "created_at values are not sorted from newest to oldest"
|
||||
|
||||
|
||||
def test_api_documents_order_created_at_asc():
|
||||
"""
|
||||
Test that the 'created_at' field is sorted in ascending order
|
||||
when the 'ordering' query parameter is set.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents_created = [
|
||||
document.created_at.isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
|
||||
documents_created.sort()
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/documents/?ordering=created_at",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
response_document_created = [
|
||||
document["created_at"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
assert (
|
||||
response_document_created == documents_created
|
||||
), "created_at values are not sorted from oldest to newest"
|
||||
|
||||
|
||||
def test_api_documents_order_updated_at_desc():
|
||||
"""
|
||||
Test that the 'updated_at' field is sorted in descending order
|
||||
when the 'ordering' query parameter is set.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Updated at next year to ensure the order is correct
|
||||
documents_updated = [
|
||||
document.updated_at.isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(
|
||||
5, is_public=True, updated_at=fake.date_time_this_year(before_now=False)
|
||||
)
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
|
||||
documents_updated.sort(reverse=True)
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/",
|
||||
"/api/v1.0/documents/?ordering=-updated_at",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -203,99 +263,97 @@ def test_api_documents_order_updated_at_desc_default():
|
||||
), "updated_at values are not sorted from newest to oldest"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ordering_field, factory_field",
|
||||
[
|
||||
("-created_at", "created_at"),
|
||||
("-updated_at", "updated_at"),
|
||||
("-title", "title"),
|
||||
],
|
||||
)
|
||||
def test_api_documents_ordering_desc(ordering_field, factory_field):
|
||||
def test_api_documents_order_updated_at_asc():
|
||||
"""
|
||||
Test that the specified field is sorted in descending order
|
||||
Test that the 'updated_at' field is sorted in ascending order
|
||||
when the 'ordering' query parameter is set.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
if factory_field == "title":
|
||||
documents_field_values = [
|
||||
factories.DocumentFactory(
|
||||
is_public=True, title=fake.sentence(nb_words=4)
|
||||
).title
|
||||
for _ in range(5)
|
||||
]
|
||||
else:
|
||||
documents_field_values = [
|
||||
getattr(document, factory_field).isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
documents_updated = [
|
||||
document.updated_at.isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
|
||||
documents_field_values.sort(reverse=True)
|
||||
documents_updated.sort()
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/?ordering={ordering_field}"
|
||||
if ordering_field != "-created_at"
|
||||
else "/api/v1.0/documents/",
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?ordering=updated_at",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
response_documents_field_values = [
|
||||
document[factory_field] for document in response_data["results"]
|
||||
response_document_updated = [
|
||||
document["updated_at"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
assert (
|
||||
response_documents_field_values == documents_field_values
|
||||
), f"{factory_field} values are not sorted as expected"
|
||||
response_document_updated == documents_updated
|
||||
), "updated_at values are not sorted from oldest to newest"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field",
|
||||
[
|
||||
("updated_at"),
|
||||
("title"),
|
||||
("created_at"),
|
||||
],
|
||||
)
|
||||
def test_api_documents_ordering_asc(field):
|
||||
def test_api_documents_order_title_desc():
|
||||
"""
|
||||
Test that the specified field is sorted in ascending order
|
||||
Test that the 'title' field is sorted in descending order
|
||||
when the 'ordering' query parameter is set.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
if field == "title":
|
||||
documents_field_values = [
|
||||
factories.DocumentFactory(
|
||||
is_public=True, title=fake.sentence(nb_words=4)
|
||||
).title
|
||||
for _ in range(5)
|
||||
]
|
||||
else:
|
||||
documents_field_values = [
|
||||
getattr(document, field).isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
documents_title = [
|
||||
factories.DocumentFactory(is_public=True, title=fake.sentence(nb_words=4)).title
|
||||
for _ in range(5)
|
||||
]
|
||||
|
||||
documents_field_values.sort()
|
||||
documents_title.sort(reverse=True)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/?ordering={field}",
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?ordering=-title",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
response_documents_field_values = [
|
||||
document[field] for document in response_data["results"]
|
||||
response_documents_title = [
|
||||
document["title"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
assert (
|
||||
response_documents_field_values == documents_field_values
|
||||
), f"{field} values are not sorted as expected"
|
||||
response_documents_title == documents_title
|
||||
), "title values are not sorted descending"
|
||||
|
||||
|
||||
def test_api_documents_order_title_asc():
|
||||
"""
|
||||
Test that the 'title' field is sorted in ascending order
|
||||
when the 'ordering' query parameter is set.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents_title = [
|
||||
factories.DocumentFactory(is_public=True, title=fake.sentence(nb_words=4)).title
|
||||
for _ in range(5)
|
||||
]
|
||||
|
||||
documents_title.sort()
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?ordering=title",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
response_documents_title = [
|
||||
document["title"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
assert (
|
||||
response_documents_title == documents_title
|
||||
), "title values are not sorted ascending"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: retrieve
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -46,7 +45,7 @@ def test_api_documents_retrieve_anonymous_not_public():
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_documents_retrieve_authenticated_unrelated_public():
|
||||
@@ -102,7 +101,7 @@ def test_api_documents_retrieve_authenticated_unrelated_not_public():
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_documents_retrieve_authenticated_related_direct():
|
||||
@@ -185,7 +184,7 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_te
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -229,8 +228,6 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
factories.TeamDocumentAccessFactory()
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||
|
||||
# pylint: disable=R0801
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
expected_abilities = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: update
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import pytest
|
||||
@@ -59,7 +58,7 @@ def test_api_documents_update_authenticated_unrelated():
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
document.refresh_from_db()
|
||||
document_values = serializers.DocumentSerializer(instance=document).data
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Test suite for generated openapi schema.
|
||||
"""
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Templates API endpoint in impress's core app: create
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Templates API endpoint in impress's core app: delete
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Test users API endpoints in the impress core app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -45,7 +44,7 @@ def test_api_templates_generate_document_anonymous_not_public():
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Template matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_templates_generate_document_authenticated_public():
|
||||
@@ -88,7 +87,7 @@ def test_api_templates_generate_document_authenticated_not_public():
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Template matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -98,7 +97,7 @@ def test_api_templates_generate_document_related(via, mock_user_get_teams):
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
access = None
|
||||
|
||||
if via == USER:
|
||||
access = factories.UserTemplateAccessFactory(user=user)
|
||||
elif via == TEAM:
|
||||
@@ -179,26 +178,3 @@ def test_api_templates_generate_document_type_unknown():
|
||||
'"unknown" is not a valid choice.',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_api_templates_generate_document_export_docx():
|
||||
"""Generate pdf document with the body type html."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
template = factories.TemplateFactory(is_public=True)
|
||||
data = {"body": "<p>Test body</p>", "body_type": "html", "format": "docx"}
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/templates/{template.id!s}/generate-document/",
|
||||
data,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (
|
||||
response.headers["content-type"]
|
||||
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Templates API endpoint in impress's core app: list
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Templates API endpoint in impress's core app: retrieve
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -42,7 +41,7 @@ def test_api_templates_retrieve_anonymous_not_public():
|
||||
response = APIClient().get(f"/api/v1.0/templates/{template.id!s}/")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Template matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_templates_retrieve_authenticated_unrelated_public():
|
||||
@@ -95,7 +94,7 @@ def test_api_templates_retrieve_authenticated_unrelated_not_public():
|
||||
f"/api/v1.0/templates/{template.id!s}/",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Template matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_templates_retrieve_authenticated_related_direct():
|
||||
@@ -175,7 +174,7 @@ def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_te
|
||||
|
||||
response = client.get(f"/api/v1.0/templates/{template.id!s}/")
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Template matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Tests for Templates API endpoint in impress's core app: update
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import pytest
|
||||
@@ -59,7 +58,7 @@ def test_api_templates_update_authenticated_unrelated():
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Template matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
template.refresh_from_db()
|
||||
template_values = serializers.TemplateSerializer(instance=template).data
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Test document accesses API endpoints for users in impress's core app.
|
||||
"""
|
||||
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -68,7 +67,6 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_get_tea
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
user_access = None
|
||||
if via == USER:
|
||||
user_access = models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
@@ -175,9 +173,7 @@ def test_api_document_accesses_retrieve_authenticated_unrelated():
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {
|
||||
"detail": "No DocumentAccess matches the given query."
|
||||
}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -216,6 +212,203 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_get
|
||||
}
|
||||
|
||||
|
||||
def test_api_document_accesses_create_anonymous():
|
||||
"""Anonymous users should not be allowed to create document accesses."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(user.id),
|
||||
"document": str(document.id),
|
||||
"role": random.choice(models.RoleChoices.choices)[0],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
assert models.DocumentAccess.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_document_accesses_create_authenticated_unrelated():
|
||||
"""
|
||||
Authenticated users should not be allowed to create document accesses for a document to
|
||||
which they are not related.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_reader_or_editor(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Readers or editors of a document should not be allowed to create document accesses."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, 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/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": new_role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
assert not models.DocumentAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_administrator(
|
||||
via, mock_user_get_teams
|
||||
):
|
||||
"""
|
||||
Administrators of a document should be able to create document accesses
|
||||
except for the "owner" role.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="administrator"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, 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/documents/{document.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 resource 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/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user_id": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
"id": str(new_document_access.id),
|
||||
"team": "",
|
||||
"role": role,
|
||||
"user": other_user,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_create_authenticated_owner(via, mock_user_get_teams):
|
||||
"""
|
||||
Owners of a document should be able to create document accesses whatever the role.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, 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/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user_id": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert models.DocumentAccess.objects.filter(user=other_user).count() == 1
|
||||
new_document_access = models.DocumentAccess.objects.filter(user=other_user).get()
|
||||
other_user = serializers.UserSerializer(instance=other_user).data
|
||||
assert response.json() == {
|
||||
"id": str(new_document_access.id),
|
||||
"user": other_user,
|
||||
"team": "",
|
||||
"role": role,
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
}
|
||||
|
||||
|
||||
def test_api_document_accesses_update_anonymous():
|
||||
"""Anonymous users should not be allowed to update a document access."""
|
||||
access = factories.UserDocumentAccessFactory()
|
||||
@@ -545,7 +738,6 @@ def test_api_document_accesses_update_owner_self(via, mock_user_get_teams):
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
access = None
|
||||
if via == USER:
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="owner"
|
||||
@@ -776,7 +968,6 @@ def test_api_document_accesses_delete_owners_last_owner(via, mock_user_get_teams
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
access = None
|
||||
if via == USER:
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
document=document, user=user, role="owner"
|
||||
@@ -1,12 +1,9 @@
|
||||
"""
|
||||
Unit tests for the Invitation model
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
|
||||
from django.core import mail
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
@@ -101,8 +98,6 @@ def test_api_document_invitations__create__privileged_members(
|
||||
"role": invited,
|
||||
}
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
@@ -113,92 +108,11 @@ def test_api_document_invitations__create__privileged_members(
|
||||
if is_allowed:
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert models.Invitation.objects.count() == 1
|
||||
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == ["guest@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
else:
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.Invitation.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_document_invitations__create__email_from_content_language():
|
||||
"""
|
||||
The email generated is from the language set in the Content-Language header
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
|
||||
invitation_values = {
|
||||
"email": "guest@example.com",
|
||||
"role": "reader",
|
||||
}
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
headers={"Content-Language": "fr-fr"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["email"] == "guest@example.com"
|
||||
assert models.Invitation.objects.count() == 1
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest@example.com"]
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation à rejoindre Docs !" in email_content
|
||||
|
||||
|
||||
def test_api_document_invitations__create__email_from_content_language_not_supported():
|
||||
"""
|
||||
If the language from the Content-Language is not supported
|
||||
it will display the default language, English.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
|
||||
|
||||
invitation_values = {
|
||||
"email": "guest@example.com",
|
||||
"role": "reader",
|
||||
}
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id}/invitations/",
|
||||
invitation_values,
|
||||
format="json",
|
||||
headers={"Content-Language": "not-supported"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["email"] == "guest@example.com"
|
||||
assert models.Invitation.objects.count() == 1
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest@example.com"]
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
|
||||
|
||||
def test_api_document_invitations__create__issuer_and_document_override():
|
||||
"""It should not be possible to set the "document" and "issuer" fields."""
|
||||
user = factories.UserFactory()
|
||||
@@ -251,7 +165,7 @@ def test_api_document_invitations__create__cannot_duplicate_invitation():
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json() == [
|
||||
assert response.json()["__all__"] == [
|
||||
"Document invitation with this Email address and Document already exists."
|
||||
]
|
||||
|
||||
@@ -279,7 +193,9 @@ def test_api_document_invitations__create__cannot_invite_existing_users():
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json() == ["This email is already associated to a registered user."]
|
||||
assert response.json()["email"] == [
|
||||
"This email is already associated to a registered user."
|
||||
]
|
||||
|
||||
|
||||
def test_api_document_invitations__list__anonymous_user():
|
||||
@@ -340,8 +256,8 @@ def test_api_document_invitations__list__authenticated(
|
||||
"is_expired": False,
|
||||
"abilities": {
|
||||
"destroy": role in ["administrator", "owner"],
|
||||
"update": role in ["administrator", "owner"],
|
||||
"partial_update": role in ["administrator", "owner"],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"retrieve": True,
|
||||
},
|
||||
}
|
||||
@@ -392,8 +308,8 @@ def test_api_document_invitations__list__expired_invitations_still_listed(settin
|
||||
"is_expired": True,
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"retrieve": True,
|
||||
},
|
||||
},
|
||||
@@ -467,87 +383,19 @@ def test_api_document_invitations__retrieve__document_member(via, mock_user_get_
|
||||
"is_expired": False,
|
||||
"abilities": {
|
||||
"destroy": role in ["administrator", "owner"],
|
||||
"update": role in ["administrator", "owner"],
|
||||
"partial_update": role in ["administrator", "owner"],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"retrieve": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations__put_authenticated(via, mock_user_get_teams):
|
||||
"""
|
||||
Authenticated user can put invitations.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
invitation = factories.InvitationFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=invitation.document, user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=invitation.document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
||||
response = client.patch(url, {"email": "test@test.test"}, format="json")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
invitation.refresh_from_db()
|
||||
assert invitation.email == "test@test.test"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations__patch_authenticated(via, mock_user_get_teams):
|
||||
"""
|
||||
Authenticated user can patch invitations.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
invitation = factories.InvitationFactory(role="owner")
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=invitation.document, user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=invitation.document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
assert invitation.role == "owner"
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
||||
response = client.patch(
|
||||
url,
|
||||
{"role": "reader"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
invitation.refresh_from_db()
|
||||
assert invitation.role == "reader"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@pytest.mark.parametrize(
|
||||
"method",
|
||||
["put", "patch"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["editor", "reader"],
|
||||
)
|
||||
def test_api_document_invitations__update__forbidden__not_authenticated(
|
||||
method, via, role, mock_user_get_teams
|
||||
):
|
||||
def test_api_document_invitations__update__forbidden(method, via, mock_user_get_teams):
|
||||
"""
|
||||
Update of invitations is currently forbidden.
|
||||
"""
|
||||
@@ -555,28 +403,24 @@ def test_api_document_invitations__update__forbidden__not_authenticated(
|
||||
invitation = factories.InvitationFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(
|
||||
document=invitation.document, user=user, role=role
|
||||
document=invitation.document, user=user, role="owner"
|
||||
)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=invitation.document, team="lasuite", role=role
|
||||
document=invitation.document, team="lasuite", role="owner"
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
url = f"/api/v1.0/documents/{invitation.document.id}/invitations/{invitation.id}/"
|
||||
|
||||
response = client.put(url)
|
||||
|
||||
if method == "put":
|
||||
response = client.put(url)
|
||||
if method == "patch":
|
||||
response = client.patch(url)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert (
|
||||
response.json()["detail"]
|
||||
== "You do not have permission to perform this action."
|
||||
)
|
||||
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json()["detail"] == f'Method "{method.upper()}" not allowed.'
|
||||
|
||||
|
||||
def test_api_document_invitations__delete__anonymous():
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Test document versions API endpoints for users in impress's core app.
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
|
||||
@@ -39,7 +38,7 @@ def test_api_document_versions_list_anonymous_private():
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_document_versions_list_authenticated_unrelated_public():
|
||||
@@ -87,7 +86,7 @@ def test_api_document_versions_list_authenticated_unrelated_private():
|
||||
f"/api/v1.0/documents/{document.id!s}/versions/",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -126,8 +125,7 @@ def test_api_document_versions_list_authenticated_related(via, mock_user_get_tea
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 0
|
||||
assert content["count"] == 0
|
||||
assert len(content["versions"]) == 0
|
||||
|
||||
# Add a new version to the document
|
||||
document.content = "new content"
|
||||
@@ -139,8 +137,9 @@ def test_api_document_versions_list_authenticated_related(via, mock_user_get_tea
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 1
|
||||
assert content["count"] == 1
|
||||
assert len(content["versions"]) == 1
|
||||
assert content["next_version_id_marker"] == ""
|
||||
assert content["is_truncated"] is False
|
||||
|
||||
|
||||
def test_api_document_versions_retrieve_anonymous_public():
|
||||
@@ -170,7 +169,7 @@ def test_api_document_versions_retrieve_anonymous_private():
|
||||
response = APIClient().get(url)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
def test_api_document_versions_retrieve_authenticated_unrelated_public():
|
||||
@@ -212,7 +211,7 @@ def test_api_document_versions_retrieve_authenticated_unrelated_private():
|
||||
f"/api/v1.0/documents/{document.id!s}/versions/{version_id:s}/",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -435,7 +434,7 @@ def test_api_document_versions_delete_authenticated_private():
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "No Document matches the given query."}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Test template accesses API endpoints for users in impress's core app.
|
||||
"""
|
||||
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -68,7 +67,6 @@ def test_api_template_accesses_list_authenticated_related(via, mock_user_get_tea
|
||||
client.force_login(user)
|
||||
|
||||
template = factories.TemplateFactory()
|
||||
user_access = None
|
||||
if via == USER:
|
||||
user_access = models.TemplateAccess.objects.create(
|
||||
template=template,
|
||||
@@ -172,9 +170,7 @@ def test_api_template_accesses_retrieve_authenticated_unrelated():
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {
|
||||
"detail": "No TemplateAccess matches the given query."
|
||||
}
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
@@ -735,7 +731,6 @@ def test_api_template_accesses_update_owner_self(via, mock_user_get_teams):
|
||||
client.force_login(user)
|
||||
|
||||
template = factories.TemplateFactory()
|
||||
access = None
|
||||
if via == USER:
|
||||
access = factories.UserTemplateAccessFactory(
|
||||
template=template, user=user, role="owner"
|
||||
@@ -960,7 +955,6 @@ def test_api_template_accesses_delete_owners_last_owner(via, mock_user_get_teams
|
||||
client.force_login(user)
|
||||
|
||||
template = factories.TemplateFactory()
|
||||
access = None
|
||||
if via == USER:
|
||||
access = factories.UserTemplateAccessFactory(
|
||||
template=template, user=user, role="owner"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Test users API endpoints in the impress core app.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Unit tests for the DocumentAccess model
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -319,7 +318,7 @@ def test_models_document_access_get_abilities_for_editor_of_administrator():
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_editor_of_editor_user(
|
||||
django_assert_num_queries,
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of editor access for the editor of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="editor")
|
||||
@@ -378,7 +377,7 @@ def test_models_document_access_get_abilities_for_reader_of_administrator():
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_reader_of_reader_user(
|
||||
django_assert_num_queries,
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of reader access for the reader of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Unit tests for the Document model
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
Unit tests for the Invitation model
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
import time
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core import exceptions
|
||||
from django.core import exceptions, mail
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
@@ -20,6 +23,13 @@ pytestmark = pytest.mark.django_db
|
||||
fake = Faker()
|
||||
|
||||
|
||||
def test_models_invitations_readonly_after_create():
|
||||
"""Existing invitations should be readonly."""
|
||||
invitation = factories.InvitationFactory()
|
||||
with pytest.raises(exceptions.PermissionDenied):
|
||||
invitation.save()
|
||||
|
||||
|
||||
def test_models_invitations_email_no_empty_mail():
|
||||
"""The "email" field should not be empty."""
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be blank"):
|
||||
@@ -158,6 +168,67 @@ def test_models_invitation__new_user__user_creation_constant_num_queries(
|
||||
models.User.objects.create(email=user_email, password="!")
|
||||
|
||||
|
||||
def test_models_document_invitations_email():
|
||||
"""Check email invitation during invitation creation."""
|
||||
member_access = factories.UserDocumentAccessFactory(role="reader")
|
||||
document = member_access.document
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
factories.UserDocumentAccessFactory(document=document)
|
||||
invitation = factories.InvitationFactory(document=document, email="john@people.com")
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == [invitation.email]
|
||||
assert email.subject == "Invitation to join Impress!"
|
||||
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Impress!" in email_content
|
||||
assert "[//example.com]" in email_content
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"django.core.mail.send_mail",
|
||||
side_effect=smtplib.SMTPException("Error SMTPException"),
|
||||
)
|
||||
@mock.patch.object(Logger, "error")
|
||||
def test_models_document_invitations_email_failed(mock_logger, _mock_send_mail):
|
||||
"""Check invitation behavior when an SMTP error occurs during invitation creation."""
|
||||
|
||||
member_access = factories.UserDocumentAccessFactory(role="reader")
|
||||
document = member_access.document
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
factories.UserDocumentAccessFactory(document=document)
|
||||
|
||||
# No error should be raised
|
||||
invitation = factories.InvitationFactory(document=document, email="john@people.com")
|
||||
|
||||
# No email has been sent
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
# Logger should be called
|
||||
mock_logger.assert_called_once()
|
||||
|
||||
(
|
||||
_,
|
||||
email,
|
||||
exception,
|
||||
) = mock_logger.call_args.args
|
||||
|
||||
assert email == invitation.email
|
||||
assert isinstance(exception, smtplib.SMTPException)
|
||||
|
||||
|
||||
# get_abilities
|
||||
|
||||
|
||||
@@ -211,8 +282,8 @@ def test_models_document_invitations_get_abilities_privileged_member(
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"partial_update": True,
|
||||
"update": True,
|
||||
"partial_update": False,
|
||||
"update": False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Unit tests for the TemplateAccess model
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -319,7 +318,7 @@ def test_models_template_access_get_abilities_for_editor_of_administrator():
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_editor_of_editor_user(
|
||||
django_assert_num_queries,
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of editor access for the editor of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="editor")
|
||||
@@ -378,7 +377,7 @@ def test_models_template_access_get_abilities_for_reader_of_administrator():
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_reader_of_reader_user(
|
||||
django_assert_num_queries,
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of reader access for the reader of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="reader")
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
"""
|
||||
Unit tests for the Template model
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -188,30 +184,3 @@ def test_models_templates_get_abilities_preset_role(django_assert_num_queries):
|
||||
"partial_update": False,
|
||||
"generate_document": True,
|
||||
}
|
||||
|
||||
|
||||
def test_models_templates__generate_word():
|
||||
"""Generate word document and assert no tmp files are left in /tmp folder."""
|
||||
template = factories.TemplateFactory()
|
||||
response = template.generate_word("<p>Test body</p>", {})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len([f for f in os.listdir("/tmp") if f.startswith("docx_")]) == 0
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"pypandoc.convert_text",
|
||||
side_effect=RuntimeError("Conversion failed"),
|
||||
)
|
||||
def test_models_templates__generate_word__raise_error(_mock_send_mail):
|
||||
"""
|
||||
Generate word document and assert no tmp files are left in /tmp folder
|
||||
even when the conversion fails.
|
||||
"""
|
||||
template = factories.TemplateFactory()
|
||||
|
||||
try:
|
||||
template.generate_word("<p>Test body</p>", {})
|
||||
except RuntimeError as e:
|
||||
assert str(e) == "Conversion failed"
|
||||
assert len([f for f in os.listdir("/tmp") if f.startswith("docx_")]) == 0
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Unit tests for the User model
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"""
|
||||
Unit tests for the Invitation model
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.core import mail
|
||||
|
||||
import pytest
|
||||
|
||||
from core.utils import email_invitation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_utils__email_invitation_success():
|
||||
"""
|
||||
The email invitation is sent successfully.
|
||||
"""
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
email_invitation("en", "guest@example.com", "123-456-789")
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation to join Docs!" in email_content
|
||||
assert "docs/123-456-789/" in email_content
|
||||
|
||||
|
||||
def test_utils__email_invitation_success_fr():
|
||||
"""
|
||||
The email invitation is sent successfully in french.
|
||||
"""
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
email_invitation("fr-fr", "guest@example.com", "123-456-789")
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 1
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == ["guest@example.com"]
|
||||
email_content = " ".join(email.body.split())
|
||||
assert "Invitation à rejoindre Docs !" in email_content
|
||||
assert "docs/123-456-789/" in email_content
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.utils.send_mail",
|
||||
side_effect=smtplib.SMTPException("Error SMTPException"),
|
||||
)
|
||||
@mock.patch.object(Logger, "error")
|
||||
def test_utils__email_invitation_failed(mock_logger, _mock_send_mail):
|
||||
"""Check mail behavior when an SMTP error occurs when sent an email invitation."""
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
email_invitation("en", "guest@example.com", "123-456-789")
|
||||
|
||||
# No email has been sent
|
||||
# pylint: disable-next=no-member
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
# Logger should be called
|
||||
mock_logger.assert_called_once()
|
||||
|
||||
(
|
||||
_,
|
||||
email,
|
||||
exception,
|
||||
) = mock_logger.call_args.args
|
||||
|
||||
assert email == "guest@example.com"
|
||||
assert isinstance(exception, smtplib.SMTPException)
|
||||
@@ -1,5 +1,4 @@
|
||||
"""URL configuration for the core app."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"""
|
||||
Utilities for the core app.
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import override
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
def email_invitation(language, email, document_id):
|
||||
"""Send email invitation."""
|
||||
try:
|
||||
with override(language):
|
||||
title = _("Invitation to join Docs!")
|
||||
template_vars = {
|
||||
"title": title,
|
||||
"site": Site.objects.get_current(),
|
||||
"document_id": document_id,
|
||||
}
|
||||
msg_html = render_to_string("mail/html/invitation.html", template_vars)
|
||||
msg_plain = render_to_string("mail/text/invitation.txt", template_vars)
|
||||
send_mail(
|
||||
title,
|
||||
msg_plain,
|
||||
settings.EMAIL_FROM,
|
||||
[email],
|
||||
html_message=msg_html,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
except smtplib.SMTPException as exception:
|
||||
logger.error("invitation to %s was not sent: %s", email, exception)
|
||||
@@ -1,7 +1,7 @@
|
||||
<page size="A4">
|
||||
<div class="header">
|
||||
<img width="200"
|
||||
src="https://impress-staging.beta.numerique.gouv.fr/assets/logo-gouv.png"
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/fr/7/72/Logo_du_Gouvernement_de_la_R%C3%A9publique_fran%C3%A7aise_%282020%29.svg"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
body {
|
||||
background: white;
|
||||
font-family: arial;
|
||||
font-family: arial
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header img {
|
||||
width: 5cm;
|
||||
margin-left: -0.4cm;
|
||||
}
|
||||
.body{
|
||||
margin-top: 1.5rem;
|
||||
margin-top: 1.5rem
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
[custom-style="center"] {
|
||||
text-align: center;
|
||||
}
|
||||
[custom-style="right"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Management user to create a superuser."""
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Impress celery configuration file."""
|
||||
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
|
||||
@@ -9,7 +9,6 @@ https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -279,7 +278,6 @@ class Base(Configuration):
|
||||
EMAIL_HOST_PASSWORD = values.Value(None)
|
||||
EMAIL_PORT = values.PositiveIntegerValue(None)
|
||||
EMAIL_USE_TLS = values.BooleanValue(False)
|
||||
EMAIL_USE_SSL = values.BooleanValue(False)
|
||||
EMAIL_FROM = values.Value("from@example.com")
|
||||
|
||||
AUTH_USER_MODEL = "core.User"
|
||||
|
||||
@@ -1,563 +1,208 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-08-14 12:43+0000\n"
|
||||
"PO-Revision-Date: 2024-08-14 12:48\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
"POT-Creation-Date: 2024-04-03 10:31+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Crowdin-Project: lasuite-people\n"
|
||||
"X-Crowdin-Project-ID: 637934\n"
|
||||
"X-Crowdin-Language: en\n"
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 8\n"
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/admin.py:31
|
||||
#: build/lib/build/lib/build/lib/core/admin.py:31
|
||||
#: build/lib/build/lib/core/admin.py:31 build/lib/core/admin.py:31
|
||||
#: core/admin.py:31
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/admin.py:33
|
||||
#: build/lib/build/lib/build/lib/core/admin.py:33
|
||||
#: build/lib/build/lib/core/admin.py:33 build/lib/core/admin.py:33
|
||||
#: core/admin.py:33
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/admin.py:45
|
||||
#: build/lib/build/lib/build/lib/core/admin.py:45
|
||||
#: build/lib/build/lib/core/admin.py:45 build/lib/core/admin.py:45
|
||||
#: core/admin.py:45
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/api/serializers.py:176
|
||||
#: build/lib/build/lib/build/lib/core/api/serializers.py:176
|
||||
#: build/lib/build/lib/core/api/serializers.py:176
|
||||
#: build/lib/core/api/serializers.py:176 core/api/serializers.py:176
|
||||
msgid "Body"
|
||||
#: core/api/serializers.py:128
|
||||
msgid "Markdown Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/api/serializers.py:179
|
||||
#: build/lib/build/lib/build/lib/core/api/serializers.py:179
|
||||
#: build/lib/build/lib/core/api/serializers.py:179
|
||||
#: build/lib/core/api/serializers.py:179 core/api/serializers.py:179
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/authentication/backends.py:71
|
||||
#: build/lib/build/lib/build/lib/core/authentication/backends.py:71
|
||||
#: build/lib/build/lib/core/authentication/backends.py:71
|
||||
#: build/lib/core/authentication/backends.py:71
|
||||
#: core/authentication/backends.py:71
|
||||
#: core/authentication.py:71
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/authentication/backends.py:91
|
||||
#: build/lib/build/lib/build/lib/core/authentication/backends.py:91
|
||||
#: build/lib/build/lib/core/authentication/backends.py:91
|
||||
#: build/lib/core/authentication/backends.py:91
|
||||
#: core/authentication/backends.py:91
|
||||
#: core/authentication.py:91
|
||||
msgid "Claims contained no recognizable user identification"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:60
|
||||
#: build/lib/build/lib/build/lib/core/models.py:60
|
||||
#: build/lib/build/lib/core/models.py:60 build/lib/core/models.py:60
|
||||
#: core/models.py:61
|
||||
msgid "Reader"
|
||||
#: core/models.py:27
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:61
|
||||
#: build/lib/build/lib/build/lib/core/models.py:61
|
||||
#: build/lib/build/lib/core/models.py:61 build/lib/core/models.py:61
|
||||
#: core/models.py:62
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:62
|
||||
#: build/lib/build/lib/build/lib/core/models.py:62
|
||||
#: build/lib/build/lib/core/models.py:62 build/lib/core/models.py:62
|
||||
#: core/models.py:63
|
||||
#: core/models.py:28
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:63
|
||||
#: build/lib/build/lib/build/lib/core/models.py:63
|
||||
#: build/lib/build/lib/core/models.py:63 build/lib/core/models.py:63
|
||||
#: core/models.py:64
|
||||
#: core/models.py:29
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:75
|
||||
#: build/lib/build/lib/build/lib/core/models.py:75
|
||||
#: build/lib/build/lib/core/models.py:75 build/lib/core/models.py:75
|
||||
#: core/models.py:76
|
||||
#: core/models.py:41
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:76
|
||||
#: build/lib/build/lib/build/lib/core/models.py:76
|
||||
#: build/lib/build/lib/core/models.py:76 build/lib/core/models.py:76
|
||||
#: core/models.py:77
|
||||
#: core/models.py:42
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:82
|
||||
#: build/lib/build/lib/build/lib/core/models.py:82
|
||||
#: build/lib/build/lib/core/models.py:82 build/lib/core/models.py:82
|
||||
#: core/models.py:83
|
||||
#: core/models.py:48
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:83
|
||||
#: build/lib/build/lib/build/lib/core/models.py:83
|
||||
#: build/lib/build/lib/core/models.py:83 build/lib/core/models.py:83
|
||||
#: core/models.py:84
|
||||
#: core/models.py:49
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:88
|
||||
#: build/lib/build/lib/build/lib/core/models.py:88
|
||||
#: build/lib/build/lib/core/models.py:88 build/lib/core/models.py:88
|
||||
#: core/models.py:89
|
||||
#: core/models.py:54
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:89
|
||||
#: build/lib/build/lib/build/lib/core/models.py:89
|
||||
#: build/lib/build/lib/core/models.py:89 build/lib/core/models.py:89
|
||||
#: core/models.py:90
|
||||
#: core/models.py:55
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:109
|
||||
#: build/lib/build/lib/build/lib/core/models.py:109
|
||||
#: build/lib/build/lib/core/models.py:109 build/lib/core/models.py:109
|
||||
#: core/models.py:110
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters."
|
||||
#: core/models.py:75
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:115
|
||||
#: build/lib/build/lib/build/lib/core/models.py:115
|
||||
#: build/lib/build/lib/core/models.py:115 build/lib/core/models.py:115
|
||||
#: core/models.py:116
|
||||
#: core/models.py:81
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:117
|
||||
#: build/lib/build/lib/build/lib/core/models.py:117
|
||||
#: build/lib/build/lib/core/models.py:117 build/lib/core/models.py:117
|
||||
#: core/models.py:118
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
#: core/models.py:83
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:125
|
||||
#: build/lib/build/lib/build/lib/core/models.py:125
|
||||
#: build/lib/build/lib/core/models.py:125 build/lib/core/models.py:125
|
||||
#: core/models.py:126
|
||||
#: core/models.py:91
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:130
|
||||
#: build/lib/build/lib/build/lib/core/models.py:130
|
||||
#: build/lib/build/lib/core/models.py:130 build/lib/core/models.py:130
|
||||
#: core/models.py:131
|
||||
#: core/models.py:96
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:137
|
||||
#: build/lib/build/lib/build/lib/core/models.py:137
|
||||
#: build/lib/build/lib/core/models.py:137 build/lib/core/models.py:137
|
||||
#: core/models.py:138
|
||||
#: core/models.py:103
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:138
|
||||
#: build/lib/build/lib/build/lib/core/models.py:138
|
||||
#: build/lib/build/lib/core/models.py:138 build/lib/core/models.py:138
|
||||
#: core/models.py:139
|
||||
#: core/models.py:104
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:144
|
||||
#: build/lib/build/lib/build/lib/core/models.py:144
|
||||
#: build/lib/build/lib/core/models.py:144 build/lib/core/models.py:144
|
||||
#: core/models.py:145
|
||||
#: core/models.py:110
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:147
|
||||
#: build/lib/build/lib/build/lib/core/models.py:147
|
||||
#: build/lib/build/lib/core/models.py:147 build/lib/core/models.py:147
|
||||
#: core/models.py:148
|
||||
#: core/models.py:113
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:149
|
||||
#: build/lib/build/lib/build/lib/core/models.py:149
|
||||
#: build/lib/build/lib/core/models.py:149 build/lib/core/models.py:149
|
||||
#: core/models.py:150
|
||||
#: core/models.py:115
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:152
|
||||
#: build/lib/build/lib/build/lib/core/models.py:152
|
||||
#: build/lib/build/lib/core/models.py:152 build/lib/core/models.py:152
|
||||
#: core/models.py:153
|
||||
#: core/models.py:118
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:154
|
||||
#: build/lib/build/lib/build/lib/core/models.py:154
|
||||
#: build/lib/build/lib/core/models.py:154 build/lib/core/models.py:154
|
||||
#: core/models.py:155
|
||||
#: core/models.py:120
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:157
|
||||
#: build/lib/build/lib/build/lib/core/models.py:157
|
||||
#: build/lib/build/lib/core/models.py:157 build/lib/core/models.py:157
|
||||
#: core/models.py:158
|
||||
#: core/models.py:123
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:160
|
||||
#: build/lib/build/lib/build/lib/core/models.py:160
|
||||
#: build/lib/build/lib/core/models.py:160 build/lib/core/models.py:160
|
||||
#: core/models.py:161
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
#: core/models.py:126
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:172
|
||||
#: build/lib/build/lib/build/lib/core/models.py:172
|
||||
#: build/lib/build/lib/core/models.py:172 build/lib/core/models.py:172
|
||||
#: core/models.py:173
|
||||
#: core/models.py:138
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:173
|
||||
#: build/lib/build/lib/build/lib/core/models.py:173
|
||||
#: build/lib/build/lib/core/models.py:173 build/lib/core/models.py:173
|
||||
#: core/models.py:174
|
||||
#: core/models.py:139
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:304
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:531
|
||||
#: build/lib/build/lib/build/lib/core/models.py:304
|
||||
#: build/lib/build/lib/build/lib/core/models.py:531
|
||||
#: build/lib/build/lib/core/models.py:304
|
||||
#: build/lib/build/lib/core/models.py:531 build/lib/core/models.py:304
|
||||
#: build/lib/core/models.py:531 core/models.py:305 core/models.py:532
|
||||
#: core/models.py:161
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:306
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:536
|
||||
#: build/lib/build/lib/build/lib/core/models.py:306
|
||||
#: build/lib/build/lib/build/lib/core/models.py:536
|
||||
#: build/lib/build/lib/core/models.py:306
|
||||
#: build/lib/build/lib/core/models.py:536 build/lib/core/models.py:306
|
||||
#: build/lib/core/models.py:536 core/models.py:307 core/models.py:537
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:308
|
||||
#: build/lib/build/lib/build/lib/core/models.py:308
|
||||
#: build/lib/build/lib/core/models.py:308 build/lib/core/models.py:308
|
||||
#: core/models.py:309
|
||||
msgid "Whether this document is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:316
|
||||
#: build/lib/build/lib/build/lib/core/models.py:316
|
||||
#: build/lib/build/lib/core/models.py:316 build/lib/core/models.py:316
|
||||
#: core/models.py:317
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:317
|
||||
#: build/lib/build/lib/build/lib/core/models.py:317
|
||||
#: build/lib/build/lib/core/models.py:317 build/lib/core/models.py:317
|
||||
#: core/models.py:318
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:495
|
||||
#: build/lib/build/lib/build/lib/core/models.py:495
|
||||
#: build/lib/build/lib/core/models.py:495 build/lib/core/models.py:495
|
||||
#: core/models.py:496
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:496
|
||||
#: build/lib/build/lib/build/lib/core/models.py:496
|
||||
#: build/lib/build/lib/core/models.py:496 build/lib/core/models.py:496
|
||||
#: core/models.py:497
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:502
|
||||
#: build/lib/build/lib/build/lib/core/models.py:502
|
||||
#: build/lib/build/lib/core/models.py:502 build/lib/core/models.py:502
|
||||
#: core/models.py:503
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:508
|
||||
#: build/lib/build/lib/build/lib/core/models.py:508
|
||||
#: build/lib/build/lib/core/models.py:508 build/lib/core/models.py:508
|
||||
#: core/models.py:509
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:514
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:691
|
||||
#: build/lib/build/lib/build/lib/core/models.py:514
|
||||
#: build/lib/build/lib/build/lib/core/models.py:691
|
||||
#: build/lib/build/lib/core/models.py:514
|
||||
#: build/lib/build/lib/core/models.py:691 build/lib/core/models.py:514
|
||||
#: build/lib/core/models.py:691 core/models.py:515 core/models.py:704
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:532
|
||||
#: build/lib/build/lib/build/lib/core/models.py:532
|
||||
#: build/lib/build/lib/core/models.py:532 build/lib/core/models.py:532
|
||||
#: core/models.py:533
|
||||
#: core/models.py:162
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:533
|
||||
#: build/lib/build/lib/build/lib/core/models.py:533
|
||||
#: build/lib/build/lib/core/models.py:533 build/lib/core/models.py:533
|
||||
#: core/models.py:534
|
||||
#: core/models.py:163
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:534
|
||||
#: build/lib/build/lib/build/lib/core/models.py:534
|
||||
#: build/lib/build/lib/core/models.py:534 build/lib/core/models.py:534
|
||||
#: core/models.py:535
|
||||
#: core/models.py:164
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:538
|
||||
#: build/lib/build/lib/build/lib/core/models.py:538
|
||||
#: build/lib/build/lib/core/models.py:538 build/lib/core/models.py:538
|
||||
#: core/models.py:539
|
||||
#: core/models.py:166
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:168
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:544
|
||||
#: build/lib/build/lib/build/lib/core/models.py:544
|
||||
#: build/lib/build/lib/core/models.py:544 build/lib/core/models.py:544
|
||||
#: core/models.py:545
|
||||
#: core/models.py:174
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:545
|
||||
#: build/lib/build/lib/build/lib/core/models.py:545
|
||||
#: build/lib/build/lib/core/models.py:545 build/lib/core/models.py:545
|
||||
#: core/models.py:546
|
||||
#: core/models.py:175
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:672
|
||||
#: build/lib/build/lib/build/lib/core/models.py:672
|
||||
#: build/lib/build/lib/core/models.py:672 build/lib/core/models.py:672
|
||||
#: core/models.py:685
|
||||
#: core/models.py:256
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:673
|
||||
#: build/lib/build/lib/build/lib/core/models.py:673
|
||||
#: build/lib/build/lib/core/models.py:673 build/lib/core/models.py:673
|
||||
#: core/models.py:686
|
||||
#: core/models.py:257
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:679
|
||||
#: build/lib/build/lib/build/lib/core/models.py:679
|
||||
#: build/lib/build/lib/core/models.py:679 build/lib/core/models.py:679
|
||||
#: core/models.py:692
|
||||
#: core/models.py:263
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:685
|
||||
#: build/lib/build/lib/build/lib/core/models.py:685
|
||||
#: build/lib/build/lib/core/models.py:685 build/lib/core/models.py:685
|
||||
#: core/models.py:698
|
||||
#: core/models.py:269
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:708
|
||||
#: build/lib/build/lib/build/lib/core/models.py:708
|
||||
#: build/lib/build/lib/core/models.py:708 build/lib/core/models.py:708
|
||||
#: core/models.py:721
|
||||
msgid "email address"
|
||||
#: core/models.py:275
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:725
|
||||
#: build/lib/build/lib/build/lib/core/models.py:725
|
||||
#: build/lib/build/lib/core/models.py:725 build/lib/core/models.py:725
|
||||
#: core/models.py:738
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:726
|
||||
#: build/lib/build/lib/build/lib/core/models.py:726
|
||||
#: build/lib/build/lib/core/models.py:726 build/lib/core/models.py:726
|
||||
#: core/models.py:739
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:751
|
||||
#: build/lib/build/lib/build/lib/core/models.py:751
|
||||
#: build/lib/build/lib/core/models.py:751 build/lib/core/models.py:751
|
||||
#: core/models.py:764
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:795
|
||||
#: build/lib/build/lib/build/lib/core/models.py:795
|
||||
#: build/lib/build/lib/core/models.py:795 build/lib/core/models.py:795
|
||||
msgid "Invitation to join Impress!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/impress/settings.py:158
|
||||
#: build/lib/build/lib/build/lib/impress/settings.py:158
|
||||
#: build/lib/build/lib/impress/settings.py:158
|
||||
#: build/lib/impress/settings.py:158 impress/settings.py:158
|
||||
#: impress/settings.py:134
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/impress/settings.py:159
|
||||
#: build/lib/build/lib/build/lib/impress/settings.py:159
|
||||
#: build/lib/build/lib/impress/settings.py:159
|
||||
#: build/lib/impress/settings.py:159 impress/settings.py:159
|
||||
#: impress/settings.py:135
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/core/api/serializers.py:185
|
||||
#: build/lib/core/api/serializers.py:185 core/api/serializers.py:185
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:808
|
||||
msgid "Invitation to join Docs!"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
|
||||
msgid "Company logo"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
#, python-format
|
||||
msgid "Hello %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
|
||||
msgid "Thank you very much for your visit!"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:221
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:160
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "La Suite Numérique"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:190
|
||||
#: core/templates/mail/text/invitation.txt:5
|
||||
msgid "Invitation to join a document !"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:198
|
||||
msgid "Welcome to <strong>Docs!</strong>"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:213
|
||||
#: core/templates/mail/text/invitation.txt:12
|
||||
msgid "We are delighted to welcome you to our community on Docs, your new companion to collaborate on documents efficiently, intuitively, and securely."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:218
|
||||
#: core/templates/mail/text/invitation.txt:13
|
||||
msgid "Our application is designed to help you organize, collaborate, and manage permissions."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:223
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
msgid "With Docs, you will be able to:"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:224
|
||||
#: core/templates/mail/text/invitation.txt:15
|
||||
msgid "Create documents."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:225
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
msgid "Work offline."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:17
|
||||
msgid "Invite members of your community to your document in just a few clicks."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:237
|
||||
#: core/templates/mail/text/invitation.txt:19
|
||||
msgid "Visit Docs"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:246
|
||||
#: core/templates/mail/text/invitation.txt:21
|
||||
msgid "We are confident that Docs will help you increase efficiency and productivity while strengthening the bond among members."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:251
|
||||
#: core/templates/mail/text/invitation.txt:22
|
||||
msgid "Feel free to explore all the features of the application and share your feedback and suggestions with us. Your feedback is valuable to us and will enable us to continually improve our service."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:256
|
||||
#: core/templates/mail/text/invitation.txt:23
|
||||
msgid "Once again, welcome aboard! We are eager to accompany you on your collaboration adventure."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:263
|
||||
#: core/templates/mail/text/invitation.txt:25
|
||||
msgid "Sincerely,"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:264
|
||||
#: core/templates/mail/text/invitation.txt:27
|
||||
msgid "The La Suite Numérique Team"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/text/hello.txt:8
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/text/invitation.txt:8
|
||||
msgid "Welcome to Docs!"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -1,563 +1,208 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-08-14 12:43+0000\n"
|
||||
"PO-Revision-Date: 2024-08-14 12:48\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
"POT-Creation-Date: 2024-04-03 10:31+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Crowdin-Project: lasuite-people\n"
|
||||
"X-Crowdin-Project-ID: 637934\n"
|
||||
"X-Crowdin-Language: fr\n"
|
||||
"X-Crowdin-File: backend-impress.pot\n"
|
||||
"X-Crowdin-File-ID: 8\n"
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/admin.py:31
|
||||
#: build/lib/build/lib/build/lib/core/admin.py:31
|
||||
#: build/lib/build/lib/core/admin.py:31 build/lib/core/admin.py:31
|
||||
#: core/admin.py:31
|
||||
msgid "Personal info"
|
||||
msgstr "Infos Personnelles"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/admin.py:33
|
||||
#: build/lib/build/lib/build/lib/core/admin.py:33
|
||||
#: build/lib/build/lib/core/admin.py:33 build/lib/core/admin.py:33
|
||||
#: core/admin.py:33
|
||||
msgid "Permissions"
|
||||
msgstr "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/admin.py:45
|
||||
#: build/lib/build/lib/build/lib/core/admin.py:45
|
||||
#: build/lib/build/lib/core/admin.py:45 build/lib/core/admin.py:45
|
||||
#: core/admin.py:45
|
||||
msgid "Important dates"
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/api/serializers.py:176
|
||||
#: build/lib/build/lib/build/lib/core/api/serializers.py:176
|
||||
#: build/lib/build/lib/core/api/serializers.py:176
|
||||
#: build/lib/core/api/serializers.py:176 core/api/serializers.py:176
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/api/serializers.py:179
|
||||
#: build/lib/build/lib/build/lib/core/api/serializers.py:179
|
||||
#: build/lib/build/lib/core/api/serializers.py:179
|
||||
#: build/lib/core/api/serializers.py:179 core/api/serializers.py:179
|
||||
msgid "Body type"
|
||||
#: core/api/serializers.py:128
|
||||
msgid "Markdown Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/authentication/backends.py:71
|
||||
#: build/lib/build/lib/build/lib/core/authentication/backends.py:71
|
||||
#: build/lib/build/lib/core/authentication/backends.py:71
|
||||
#: build/lib/core/authentication/backends.py:71
|
||||
#: core/authentication/backends.py:71
|
||||
#: core/authentication.py:71
|
||||
msgid "User info contained no recognizable user identification"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/authentication/backends.py:91
|
||||
#: build/lib/build/lib/build/lib/core/authentication/backends.py:91
|
||||
#: build/lib/build/lib/core/authentication/backends.py:91
|
||||
#: build/lib/core/authentication/backends.py:91
|
||||
#: core/authentication/backends.py:91
|
||||
#: core/authentication.py:91
|
||||
msgid "Claims contained no recognizable user identification"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:60
|
||||
#: build/lib/build/lib/build/lib/core/models.py:60
|
||||
#: build/lib/build/lib/core/models.py:60 build/lib/core/models.py:60
|
||||
#: core/models.py:61
|
||||
msgid "Reader"
|
||||
#: core/models.py:27
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:61
|
||||
#: build/lib/build/lib/build/lib/core/models.py:61
|
||||
#: build/lib/build/lib/core/models.py:61 build/lib/core/models.py:61
|
||||
#: core/models.py:62
|
||||
msgid "Editor"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:62
|
||||
#: build/lib/build/lib/build/lib/core/models.py:62
|
||||
#: build/lib/build/lib/core/models.py:62 build/lib/core/models.py:62
|
||||
#: core/models.py:63
|
||||
#: core/models.py:28
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:63
|
||||
#: build/lib/build/lib/build/lib/core/models.py:63
|
||||
#: build/lib/build/lib/core/models.py:63 build/lib/core/models.py:63
|
||||
#: core/models.py:64
|
||||
#: core/models.py:29
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:75
|
||||
#: build/lib/build/lib/build/lib/core/models.py:75
|
||||
#: build/lib/build/lib/core/models.py:75 build/lib/core/models.py:75
|
||||
#: core/models.py:76
|
||||
#: core/models.py:41
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:76
|
||||
#: build/lib/build/lib/build/lib/core/models.py:76
|
||||
#: build/lib/build/lib/core/models.py:76 build/lib/core/models.py:76
|
||||
#: core/models.py:77
|
||||
#: core/models.py:42
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:82
|
||||
#: build/lib/build/lib/build/lib/core/models.py:82
|
||||
#: build/lib/build/lib/core/models.py:82 build/lib/core/models.py:82
|
||||
#: core/models.py:83
|
||||
#: core/models.py:48
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:83
|
||||
#: build/lib/build/lib/build/lib/core/models.py:83
|
||||
#: build/lib/build/lib/core/models.py:83 build/lib/core/models.py:83
|
||||
#: core/models.py:84
|
||||
#: core/models.py:49
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:88
|
||||
#: build/lib/build/lib/build/lib/core/models.py:88
|
||||
#: build/lib/build/lib/core/models.py:88 build/lib/core/models.py:88
|
||||
#: core/models.py:89
|
||||
#: core/models.py:54
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:89
|
||||
#: build/lib/build/lib/build/lib/core/models.py:89
|
||||
#: build/lib/build/lib/core/models.py:89 build/lib/core/models.py:89
|
||||
#: core/models.py:90
|
||||
#: core/models.py:55
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:109
|
||||
#: build/lib/build/lib/build/lib/core/models.py:109
|
||||
#: build/lib/build/lib/core/models.py:109 build/lib/core/models.py:109
|
||||
#: core/models.py:110
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters."
|
||||
#: core/models.py:75
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:115
|
||||
#: build/lib/build/lib/build/lib/core/models.py:115
|
||||
#: build/lib/build/lib/core/models.py:115 build/lib/core/models.py:115
|
||||
#: core/models.py:116
|
||||
#: core/models.py:81
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:117
|
||||
#: build/lib/build/lib/build/lib/core/models.py:117
|
||||
#: build/lib/build/lib/core/models.py:117 build/lib/core/models.py:117
|
||||
#: core/models.py:118
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
#: core/models.py:83
|
||||
msgid ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:125
|
||||
#: build/lib/build/lib/build/lib/core/models.py:125
|
||||
#: build/lib/build/lib/core/models.py:125 build/lib/core/models.py:125
|
||||
#: core/models.py:126
|
||||
#: core/models.py:91
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:130
|
||||
#: build/lib/build/lib/build/lib/core/models.py:130
|
||||
#: build/lib/build/lib/core/models.py:130 build/lib/core/models.py:130
|
||||
#: core/models.py:131
|
||||
#: core/models.py:96
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:137
|
||||
#: build/lib/build/lib/build/lib/core/models.py:137
|
||||
#: build/lib/build/lib/core/models.py:137 build/lib/core/models.py:137
|
||||
#: core/models.py:138
|
||||
#: core/models.py:103
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:138
|
||||
#: build/lib/build/lib/build/lib/core/models.py:138
|
||||
#: build/lib/build/lib/core/models.py:138 build/lib/core/models.py:138
|
||||
#: core/models.py:139
|
||||
#: core/models.py:104
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:144
|
||||
#: build/lib/build/lib/build/lib/core/models.py:144
|
||||
#: build/lib/build/lib/core/models.py:144 build/lib/core/models.py:144
|
||||
#: core/models.py:145
|
||||
#: core/models.py:110
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:147
|
||||
#: build/lib/build/lib/build/lib/core/models.py:147
|
||||
#: build/lib/build/lib/core/models.py:147 build/lib/core/models.py:147
|
||||
#: core/models.py:148
|
||||
#: core/models.py:113
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:149
|
||||
#: build/lib/build/lib/build/lib/core/models.py:149
|
||||
#: build/lib/build/lib/core/models.py:149 build/lib/core/models.py:149
|
||||
#: core/models.py:150
|
||||
#: core/models.py:115
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:152
|
||||
#: build/lib/build/lib/build/lib/core/models.py:152
|
||||
#: build/lib/build/lib/core/models.py:152 build/lib/core/models.py:152
|
||||
#: core/models.py:153
|
||||
#: core/models.py:118
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:154
|
||||
#: build/lib/build/lib/build/lib/core/models.py:154
|
||||
#: build/lib/build/lib/core/models.py:154 build/lib/core/models.py:154
|
||||
#: core/models.py:155
|
||||
#: core/models.py:120
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:157
|
||||
#: build/lib/build/lib/build/lib/core/models.py:157
|
||||
#: build/lib/build/lib/core/models.py:157 build/lib/core/models.py:157
|
||||
#: core/models.py:158
|
||||
#: core/models.py:123
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:160
|
||||
#: build/lib/build/lib/build/lib/core/models.py:160
|
||||
#: build/lib/build/lib/core/models.py:160 build/lib/core/models.py:160
|
||||
#: core/models.py:161
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
#: core/models.py:126
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:172
|
||||
#: build/lib/build/lib/build/lib/core/models.py:172
|
||||
#: build/lib/build/lib/core/models.py:172 build/lib/core/models.py:172
|
||||
#: core/models.py:173
|
||||
#: core/models.py:138
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:173
|
||||
#: build/lib/build/lib/build/lib/core/models.py:173
|
||||
#: build/lib/build/lib/core/models.py:173 build/lib/core/models.py:173
|
||||
#: core/models.py:174
|
||||
#: core/models.py:139
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:304
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:531
|
||||
#: build/lib/build/lib/build/lib/core/models.py:304
|
||||
#: build/lib/build/lib/build/lib/core/models.py:531
|
||||
#: build/lib/build/lib/core/models.py:304
|
||||
#: build/lib/build/lib/core/models.py:531 build/lib/core/models.py:304
|
||||
#: build/lib/core/models.py:531 core/models.py:305 core/models.py:532
|
||||
#: core/models.py:161
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:306
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:536
|
||||
#: build/lib/build/lib/build/lib/core/models.py:306
|
||||
#: build/lib/build/lib/build/lib/core/models.py:536
|
||||
#: build/lib/build/lib/core/models.py:306
|
||||
#: build/lib/build/lib/core/models.py:536 build/lib/core/models.py:306
|
||||
#: build/lib/core/models.py:536 core/models.py:307 core/models.py:537
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:308
|
||||
#: build/lib/build/lib/build/lib/core/models.py:308
|
||||
#: build/lib/build/lib/core/models.py:308 build/lib/core/models.py:308
|
||||
#: core/models.py:309
|
||||
msgid "Whether this document is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:316
|
||||
#: build/lib/build/lib/build/lib/core/models.py:316
|
||||
#: build/lib/build/lib/core/models.py:316 build/lib/core/models.py:316
|
||||
#: core/models.py:317
|
||||
msgid "Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:317
|
||||
#: build/lib/build/lib/build/lib/core/models.py:317
|
||||
#: build/lib/build/lib/core/models.py:317 build/lib/core/models.py:317
|
||||
#: core/models.py:318
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:495
|
||||
#: build/lib/build/lib/build/lib/core/models.py:495
|
||||
#: build/lib/build/lib/core/models.py:495 build/lib/core/models.py:495
|
||||
#: core/models.py:496
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:496
|
||||
#: build/lib/build/lib/build/lib/core/models.py:496
|
||||
#: build/lib/build/lib/core/models.py:496 build/lib/core/models.py:496
|
||||
#: core/models.py:497
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:502
|
||||
#: build/lib/build/lib/build/lib/core/models.py:502
|
||||
#: build/lib/build/lib/core/models.py:502 build/lib/core/models.py:502
|
||||
#: core/models.py:503
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:508
|
||||
#: build/lib/build/lib/build/lib/core/models.py:508
|
||||
#: build/lib/build/lib/core/models.py:508 build/lib/core/models.py:508
|
||||
#: core/models.py:509
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:514
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:691
|
||||
#: build/lib/build/lib/build/lib/core/models.py:514
|
||||
#: build/lib/build/lib/build/lib/core/models.py:691
|
||||
#: build/lib/build/lib/core/models.py:514
|
||||
#: build/lib/build/lib/core/models.py:691 build/lib/core/models.py:514
|
||||
#: build/lib/core/models.py:691 core/models.py:515 core/models.py:704
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:532
|
||||
#: build/lib/build/lib/build/lib/core/models.py:532
|
||||
#: build/lib/build/lib/core/models.py:532 build/lib/core/models.py:532
|
||||
#: core/models.py:533
|
||||
#: core/models.py:162
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:533
|
||||
#: build/lib/build/lib/build/lib/core/models.py:533
|
||||
#: build/lib/build/lib/core/models.py:533 build/lib/core/models.py:533
|
||||
#: core/models.py:534
|
||||
#: core/models.py:163
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:534
|
||||
#: build/lib/build/lib/build/lib/core/models.py:534
|
||||
#: build/lib/build/lib/core/models.py:534 build/lib/core/models.py:534
|
||||
#: core/models.py:535
|
||||
#: core/models.py:164
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:538
|
||||
#: build/lib/build/lib/build/lib/core/models.py:538
|
||||
#: build/lib/build/lib/core/models.py:538 build/lib/core/models.py:538
|
||||
#: core/models.py:539
|
||||
#: core/models.py:166
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:168
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:544
|
||||
#: build/lib/build/lib/build/lib/core/models.py:544
|
||||
#: build/lib/build/lib/core/models.py:544 build/lib/core/models.py:544
|
||||
#: core/models.py:545
|
||||
#: core/models.py:174
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:545
|
||||
#: build/lib/build/lib/build/lib/core/models.py:545
|
||||
#: build/lib/build/lib/core/models.py:545 build/lib/core/models.py:545
|
||||
#: core/models.py:546
|
||||
#: core/models.py:175
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:672
|
||||
#: build/lib/build/lib/build/lib/core/models.py:672
|
||||
#: build/lib/build/lib/core/models.py:672 build/lib/core/models.py:672
|
||||
#: core/models.py:685
|
||||
#: core/models.py:256
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:673
|
||||
#: build/lib/build/lib/build/lib/core/models.py:673
|
||||
#: build/lib/build/lib/core/models.py:673 build/lib/core/models.py:673
|
||||
#: core/models.py:686
|
||||
#: core/models.py:257
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:679
|
||||
#: build/lib/build/lib/build/lib/core/models.py:679
|
||||
#: build/lib/build/lib/core/models.py:679 build/lib/core/models.py:679
|
||||
#: core/models.py:692
|
||||
#: core/models.py:263
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:685
|
||||
#: build/lib/build/lib/build/lib/core/models.py:685
|
||||
#: build/lib/build/lib/core/models.py:685 build/lib/core/models.py:685
|
||||
#: core/models.py:698
|
||||
#: core/models.py:269
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:708
|
||||
#: build/lib/build/lib/build/lib/core/models.py:708
|
||||
#: build/lib/build/lib/core/models.py:708 build/lib/core/models.py:708
|
||||
#: core/models.py:721
|
||||
msgid "email address"
|
||||
#: core/models.py:275
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:725
|
||||
#: build/lib/build/lib/build/lib/core/models.py:725
|
||||
#: build/lib/build/lib/core/models.py:725 build/lib/core/models.py:725
|
||||
#: core/models.py:738
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:726
|
||||
#: build/lib/build/lib/build/lib/core/models.py:726
|
||||
#: build/lib/build/lib/core/models.py:726 build/lib/core/models.py:726
|
||||
#: core/models.py:739
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:751
|
||||
#: build/lib/build/lib/build/lib/core/models.py:751
|
||||
#: build/lib/build/lib/core/models.py:751 build/lib/core/models.py:751
|
||||
#: core/models.py:764
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/core/models.py:795
|
||||
#: build/lib/build/lib/build/lib/core/models.py:795
|
||||
#: build/lib/build/lib/core/models.py:795 build/lib/core/models.py:795
|
||||
msgid "Invitation to join Impress!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/impress/settings.py:158
|
||||
#: build/lib/build/lib/build/lib/impress/settings.py:158
|
||||
#: build/lib/build/lib/impress/settings.py:158
|
||||
#: build/lib/impress/settings.py:158 impress/settings.py:158
|
||||
#: impress/settings.py:134
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/build/lib/build/lib/impress/settings.py:159
|
||||
#: build/lib/build/lib/build/lib/impress/settings.py:159
|
||||
#: build/lib/build/lib/impress/settings.py:159
|
||||
#: build/lib/impress/settings.py:159 impress/settings.py:159
|
||||
#: impress/settings.py:135
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/build/lib/core/api/serializers.py:185
|
||||
#: build/lib/core/api/serializers.py:185 core/api/serializers.py:185
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:808
|
||||
msgid "Invitation to join Docs!"
|
||||
msgstr "Invitation à rejoindre Docs !"
|
||||
|
||||
#: core/templates/mail/html/hello.html:159 core/templates/mail/text/hello.txt:3
|
||||
msgid "Company logo"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
#, python-format
|
||||
msgid "Hello %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:188 core/templates/mail/text/hello.txt:5
|
||||
msgid "Hello"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:189 core/templates/mail/text/hello.txt:6
|
||||
msgid "Thank you very much for your visit!"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/hello.html:221
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:160
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "La Suite Numérique"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/invitation.html:190
|
||||
#: core/templates/mail/text/invitation.txt:5
|
||||
msgid "Invitation to join a document !"
|
||||
msgstr "Invitation à rejoindre un document !"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:198
|
||||
msgid "Welcome to <strong>Docs!</strong>"
|
||||
msgstr "Bienvenue sur <strong>Docs !</strong>"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:213
|
||||
#: core/templates/mail/text/invitation.txt:12
|
||||
msgid "We are delighted to welcome you to our community on Docs, your new companion to collaborate on documents efficiently, intuitively, and securely."
|
||||
msgstr "Nous sommes heureux de vous accueillir dans notre communauté sur Docs, votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement, et en toute sécurité."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:218
|
||||
#: core/templates/mail/text/invitation.txt:13
|
||||
msgid "Our application is designed to help you organize, collaborate, and manage permissions."
|
||||
msgstr "Notre application est conçue pour vous aider à organiser, collaborer et gérer vos permissions."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:223
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
msgid "With Docs, you will be able to:"
|
||||
msgstr "Avec Docs, vous serez capable de :"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:224
|
||||
#: core/templates/mail/text/invitation.txt:15
|
||||
msgid "Create documents."
|
||||
msgstr "Créez des documents."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:225
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
msgid "Work offline."
|
||||
msgstr "Travailler hors ligne."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:17
|
||||
msgid "Invite members of your community to your document in just a few clicks."
|
||||
msgstr "Invitez des membres de votre communauté sur votre document en quelques clics."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:237
|
||||
#: core/templates/mail/text/invitation.txt:19
|
||||
msgid "Visit Docs"
|
||||
msgstr "Visitez Docs"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:246
|
||||
#: core/templates/mail/text/invitation.txt:21
|
||||
msgid "We are confident that Docs will help you increase efficiency and productivity while strengthening the bond among members."
|
||||
msgstr "Nous sommes persuadés que Docs vous aidera à améliorer votre efficacité et votre productivité tout en renforçant les liens entre vos membres."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:251
|
||||
#: core/templates/mail/text/invitation.txt:22
|
||||
msgid "Feel free to explore all the features of the application and share your feedback and suggestions with us. Your feedback is valuable to us and will enable us to continually improve our service."
|
||||
msgstr "N'hésitez pas à explorer toutes les fonctionnalités de l'application et à nous faire part de vos commentaires et suggestions. Vos commentaires nous sont précieux et nous permettront d'améliorer continuellement notre service."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:256
|
||||
#: core/templates/mail/text/invitation.txt:23
|
||||
msgid "Once again, welcome aboard! We are eager to accompany you on your collaboration adventure."
|
||||
msgstr "Encore une fois, bienvenue à bord ! Nous sommes impatients de vous accompagner dans votre aventure collaborative."
|
||||
|
||||
#: core/templates/mail/html/invitation.html:263
|
||||
#: core/templates/mail/text/invitation.txt:25
|
||||
msgid "Sincerely,"
|
||||
msgstr "Sincèrement,"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:264
|
||||
#: core/templates/mail/text/invitation.txt:27
|
||||
msgid "The La Suite Numérique Team"
|
||||
msgstr "L'équipe La Suite Numérique"
|
||||
|
||||
#: core/templates/mail/text/hello.txt:8
|
||||
#, python-format
|
||||
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/text/invitation.txt:8
|
||||
msgid "Welcome to Docs!"
|
||||
msgstr "Bienvenue sur Docs !"
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"""
|
||||
impress's sandbox management script.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "1.2.1"
|
||||
version = "1.1.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,38 +25,37 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.35.0",
|
||||
"boto3==1.33.6",
|
||||
"Brotli==1.1.0",
|
||||
"celery[redis]==5.4.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.4.0",
|
||||
"django-countries==7.6.1",
|
||||
"celery[redis]==5.3.6",
|
||||
"django-configurations==2.5",
|
||||
"django-cors-headers==4.3.1",
|
||||
"django-countries==7.5.1",
|
||||
"django-parler==2.3",
|
||||
"redis==5.0.8",
|
||||
"redis==5.0.3",
|
||||
"django-redis==5.4.0",
|
||||
"django-storages[s3]==1.14.2",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.0.8",
|
||||
"djangorestframework==3.15.2",
|
||||
"drf_spectacular==0.27.2",
|
||||
"dockerflow==2024.4.2",
|
||||
"easy_thumbnails==2.9",
|
||||
"factory_boy==3.3.1",
|
||||
"freezegun==1.5.1",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.23.0",
|
||||
"markdown==3.7",
|
||||
"django==5.0.7",
|
||||
"djangorestframework==3.14.0",
|
||||
"drf_spectacular==0.26.5",
|
||||
"dockerflow==2022.8.0",
|
||||
"easy_thumbnails==2.8.5",
|
||||
"factory_boy==3.3.0",
|
||||
"freezegun==1.5.0",
|
||||
"gunicorn==22.0.0",
|
||||
"jsonschema==4.20.0",
|
||||
"markdown==3.5.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"psycopg[binary]==3.2.1",
|
||||
"PyJWT==2.9.0",
|
||||
"pypandoc==1.13",
|
||||
"python-frontmatter==1.1.0",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk==2.13.0",
|
||||
"psycopg[binary]==3.1.14",
|
||||
"PyJWT==2.8.0",
|
||||
"python-frontmatter==1.0.1",
|
||||
"requests==2.32.2",
|
||||
"sentry-sdk==1.38.0",
|
||||
"url-normalize==1.4.3",
|
||||
"WeasyPrint>=60.2",
|
||||
"whitenoise==6.7.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"whitenoise==6.6.0",
|
||||
"mozilla-django-oidc==4.0.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -68,20 +67,20 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==3.2.3",
|
||||
"drf-spectacular-sidecar==2024.7.1",
|
||||
"drf-spectacular-sidecar==2023.12.1",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==8.26.0",
|
||||
"pyfakefs==5.6.0",
|
||||
"ipython==8.18.1",
|
||||
"pyfakefs==5.3.2",
|
||||
"pylint-django==2.5.5",
|
||||
"pylint==3.2.6",
|
||||
"pytest-cov==5.0.0",
|
||||
"pytest-django==4.8.0",
|
||||
"pytest==8.3.2",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.6.1",
|
||||
"responses==0.25.3",
|
||||
"ruff==0.6.1",
|
||||
"types-requests==2.32.0.20240712",
|
||||
"pylint==3.0.3",
|
||||
"pytest-cov==4.1.0",
|
||||
"pytest-django==4.7.0",
|
||||
"pytest==7.4.3",
|
||||
"pytest-icdiff==0.8",
|
||||
"pytest-xdist==3.5.0",
|
||||
"responses==0.24.1",
|
||||
"ruff==0.1.6",
|
||||
"types-requests==2.31.0.10",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
@@ -100,11 +99,11 @@ exclude = [
|
||||
"__pycache__",
|
||||
"*/migrations/*",
|
||||
]
|
||||
ignore= ["DJ001", "PLR2004"]
|
||||
line-length = 88
|
||||
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = ["DJ001", "PLR2004"]
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"BLE", # flake8-blind-except
|
||||
@@ -126,7 +125,7 @@ select = [
|
||||
section-order = ["future","standard-library","django","third-party","impress","first-party","local-folder"]
|
||||
sections = { impress=["core"], django=["django"] }
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
[tool.ruff.per-file-ignores]
|
||||
"**/tests/*" = ["S", "SLF"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
@@ -61,15 +61,6 @@ FROM impress as impress-builder
|
||||
|
||||
WORKDIR /home/frontend/apps/impress
|
||||
|
||||
ARG FRONTEND_THEME
|
||||
ENV NEXT_PUBLIC_THEME=${FRONTEND_THEME}
|
||||
|
||||
ARG SIGNALING_URL
|
||||
ENV NEXT_PUBLIC_SIGNALING_URL=${SIGNALING_URL}
|
||||
|
||||
ARG API_ORIGIN
|
||||
ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN}
|
||||
|
||||
RUN yarn build
|
||||
|
||||
# ---- Front-end image ----
|
||||
|
||||
@@ -17,11 +17,3 @@ setup('authenticate-webkit', async ({ page }) => {
|
||||
.context()
|
||||
.storageState({ path: `playwright/.auth/user-webkit.json` });
|
||||
});
|
||||
|
||||
setup('authenticate-firefox', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, 'firefox');
|
||||
await page
|
||||
.context()
|
||||
.storageState({ path: `playwright/.auth/user-firefox.json` });
|
||||
});
|
||||
|
||||
@@ -140,34 +140,3 @@ export const goToGridDoc = async (
|
||||
|
||||
return docTitle as string;
|
||||
};
|
||||
|
||||
export const mockedDocument = async (page: Page, json: object) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET') && !request.url().includes('page=')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'mocked-document-id',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
...json,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||
import { createDoc, goToGridDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Editor', () => {
|
||||
test('checks the Doc Editor interact correctly', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
await expect(page.getByText('Hello World')).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the Doc is connected to the webrtc server', async ({
|
||||
page,
|
||||
browserName,
|
||||
@@ -140,23 +153,42 @@ test.describe('Doc Editor', () => {
|
||||
});
|
||||
|
||||
test('it cannot edit if viewer', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(
|
||||
page.getByText('Read only, you cannot edit this document.'),
|
||||
page.getByText(
|
||||
"Read only, you don't have the right to update this document.",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import jsdom from 'jsdom';
|
||||
import pdf from 'pdf-parse';
|
||||
|
||||
import { createDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Export', () => {
|
||||
test('it converts the doc to pdf with a template integrated', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
})
|
||||
.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfText = (await pdf(pdfBuffer)).text;
|
||||
|
||||
expect(pdfText).toContain('Hello World'); // This is the doc text
|
||||
});
|
||||
|
||||
test('it converts the doc to docx with a template integrated', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.docx`);
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByText('Docx').click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
})
|
||||
.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`);
|
||||
});
|
||||
|
||||
test('it converts the blocknote json in correct html for the export', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.slow();
|
||||
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
let body = '';
|
||||
|
||||
await page.route('**/templates/*/generate-document/', async (route) => {
|
||||
const request = route.request();
|
||||
body = request.postDataJSON().body;
|
||||
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('Hello World');
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('Break');
|
||||
await expect(page.getByText('Break')).toBeVisible();
|
||||
|
||||
// Center the text
|
||||
await page.getByText('Break').dblclick();
|
||||
await page.locator('button[data-test="alignTextCenter"]').click();
|
||||
|
||||
// Change the background color
|
||||
await page.locator('button[data-test="colors"]').click();
|
||||
await page.locator('button[data-test="background-color-brown"]').click();
|
||||
|
||||
// Change the text color
|
||||
await page.getByText('Break').dblclick();
|
||||
await page.locator('button[data-test="colors"]').click();
|
||||
await page.locator('button[data-test="text-color-orange"]').click();
|
||||
|
||||
// Add a list
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Bullet List').click();
|
||||
await page
|
||||
.locator('.bn-block-content[data-content-type="bulletListItem"] p')
|
||||
.last()
|
||||
.fill('Test List 1');
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('Enter');
|
||||
await page
|
||||
.locator('.bn-block-content[data-content-type="bulletListItem"] p')
|
||||
.last()
|
||||
.fill('Test List 2');
|
||||
await page.keyboard.press('Enter');
|
||||
await page
|
||||
.locator('.bn-block-content[data-content-type="bulletListItem"] p')
|
||||
.last()
|
||||
.fill('Test List 3');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Backspace');
|
||||
|
||||
// Add a number list
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Numbered List').click();
|
||||
await page
|
||||
.locator('.bn-block-content[data-content-type="numberedListItem"] p')
|
||||
.last()
|
||||
.fill('Test Number 1');
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(300);
|
||||
await page.keyboard.press('Enter');
|
||||
await page
|
||||
.locator('.bn-block-content[data-content-type="numberedListItem"] p')
|
||||
.last()
|
||||
.fill('Test Number 2');
|
||||
await page.keyboard.press('Enter');
|
||||
await page
|
||||
.locator('.bn-block-content[data-content-type="numberedListItem"] p')
|
||||
.last()
|
||||
.fill('Test Number 3');
|
||||
|
||||
// Add img
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('/');
|
||||
await page
|
||||
.getByRole('option', {
|
||||
name: 'Image',
|
||||
})
|
||||
.click();
|
||||
await page
|
||||
.getByPlaceholder('Enter URL')
|
||||
.fill('https://example.com/image.jpg');
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Embed image',
|
||||
})
|
||||
.click();
|
||||
|
||||
// Download
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
})
|
||||
.click();
|
||||
|
||||
// Empty paragraph should be replaced by a <br/>
|
||||
expect(body.match(/<br>/g)?.length).toBeGreaterThanOrEqual(2);
|
||||
expect(body).toContain('style="color: orange;"');
|
||||
expect(body).toContain('custom-style="center"');
|
||||
expect(body).toContain('style="background-color: brown;"');
|
||||
|
||||
const { JSDOM } = jsdom;
|
||||
const DOMParser = new JSDOM().window.DOMParser;
|
||||
const parser = new DOMParser();
|
||||
const html = parser.parseFromString(body, 'text/html');
|
||||
|
||||
const ulLis = html.querySelectorAll('ul li');
|
||||
expect(ulLis.length).toBe(3);
|
||||
expect(ulLis[0].textContent).toBe('Test List 1');
|
||||
expect(ulLis[1].textContent).toBe('Test List 2');
|
||||
expect(ulLis[2].textContent).toBe('Test List 3');
|
||||
|
||||
const olLis = html.querySelectorAll('ol li');
|
||||
expect(olLis.length).toBe(3);
|
||||
expect(olLis[0].textContent).toBe('Test Number 1');
|
||||
expect(olLis[1].textContent).toBe('Test Number 2');
|
||||
expect(olLis[2].textContent).toBe('Test Number 3');
|
||||
|
||||
const img = html.querySelectorAll('img');
|
||||
expect(img.length).toBe(1);
|
||||
expect(img[0].src).toBe('https://example.com/image.jpg');
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ test.describe('Documents Grid', () => {
|
||||
await expect(thead.getByText(/Created at/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Updated at/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Your role/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Members/i)).toBeVisible();
|
||||
await expect(thead.getByText(/Users number/i)).toBeVisible();
|
||||
|
||||
const row1 = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||
const docName = await row1.nth(1).textContent();
|
||||
@@ -49,130 +49,111 @@ test.describe('Documents Grid', () => {
|
||||
nameColumn: 'Document name',
|
||||
ordering: 'title',
|
||||
cellNumber: 1,
|
||||
orderDefault: '',
|
||||
orderDesc: '&ordering=-title',
|
||||
orderAsc: '&ordering=title',
|
||||
},
|
||||
{
|
||||
nameColumn: 'Created at',
|
||||
ordering: 'created_at',
|
||||
cellNumber: 2,
|
||||
orderDefault: '',
|
||||
orderDesc: '&ordering=-created_at',
|
||||
orderAsc: '&ordering=created_at',
|
||||
},
|
||||
{
|
||||
nameColumn: 'Updated at',
|
||||
ordering: 'updated_at',
|
||||
cellNumber: 3,
|
||||
orderDefault: '&ordering=-updated_at',
|
||||
orderDesc: '&ordering=updated_at',
|
||||
orderAsc: '',
|
||||
},
|
||||
].forEach(
|
||||
({
|
||||
nameColumn,
|
||||
ordering,
|
||||
cellNumber,
|
||||
orderDefault,
|
||||
orderDesc,
|
||||
orderAsc,
|
||||
}) => {
|
||||
test(`checks datagrid ordering ${ordering}`, async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1${orderDefault}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
].forEach(({ nameColumn, ordering, cellNumber }) => {
|
||||
test(`checks datagrid ordering ${ordering}`, async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromiseOrderingDesc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1${orderDesc}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const responsePromiseOrderingDesc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1&ordering=-${ordering}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromiseOrderingAsc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1${orderAsc}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const responsePromiseOrderingAsc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1&ordering=${ordering}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
// Checks the initial state
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
const thead = datagrid.locator('thead');
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
const thead = datagrid.locator('thead');
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const docNameRow1 = datagrid
|
||||
.getByRole('row')
|
||||
.nth(1)
|
||||
.getByRole('cell')
|
||||
.nth(cellNumber);
|
||||
const docNameRow2 = datagrid
|
||||
.getByRole('row')
|
||||
.nth(2)
|
||||
.getByRole('cell')
|
||||
.nth(cellNumber);
|
||||
const docNameRow1 = datagrid
|
||||
.getByRole('row')
|
||||
.nth(1)
|
||||
.getByRole('cell')
|
||||
.nth(cellNumber);
|
||||
const docNameRow2 = datagrid
|
||||
.getByRole('row')
|
||||
.nth(2)
|
||||
.getByRole('cell')
|
||||
.nth(cellNumber);
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
|
||||
// Initial state
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const initialDocNameRow1 = await docNameRow1.textContent();
|
||||
const initialDocNameRow2 = await docNameRow2.textContent();
|
||||
// Initial state
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const initialDocNameRow1 = await docNameRow1.textContent();
|
||||
const initialDocNameRow2 = await docNameRow2.textContent();
|
||||
|
||||
expect(initialDocNameRow1).toBeDefined();
|
||||
expect(initialDocNameRow2).toBeDefined();
|
||||
expect(initialDocNameRow1).toBeDefined();
|
||||
expect(initialDocNameRow2).toBeDefined();
|
||||
|
||||
// Ordering ASC
|
||||
await thead.getByText(nameColumn).click();
|
||||
// Ordering ASC
|
||||
await thead.getByText(nameColumn).click();
|
||||
|
||||
const responseOrderingAsc = await responsePromiseOrderingAsc;
|
||||
expect(responseOrderingAsc.ok()).toBeTruthy();
|
||||
const responseOrderingAsc = await responsePromiseOrderingAsc;
|
||||
expect(responseOrderingAsc.ok()).toBeTruthy();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const textDocNameRow1Asc = await docNameRow1.textContent();
|
||||
const textDocNameRow2Asc = await docNameRow2.textContent();
|
||||
expect(
|
||||
textDocNameRow1Asc &&
|
||||
textDocNameRow2Asc &&
|
||||
textDocNameRow1Asc.localeCompare(textDocNameRow2Asc, 'en', {
|
||||
caseFirst: 'false',
|
||||
ignorePunctuation: true,
|
||||
}) <= 0,
|
||||
).toBeTruthy();
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const textDocNameRow1Asc = await docNameRow1.textContent();
|
||||
const textDocNameRow2Asc = await docNameRow2.textContent();
|
||||
expect(
|
||||
textDocNameRow1Asc &&
|
||||
textDocNameRow2Asc &&
|
||||
textDocNameRow1Asc.localeCompare(textDocNameRow2Asc, 'en', {
|
||||
caseFirst: 'false',
|
||||
ignorePunctuation: true,
|
||||
}) <= 0,
|
||||
).toBeTruthy();
|
||||
|
||||
// Ordering Desc
|
||||
await thead.getByText(nameColumn).click();
|
||||
// Ordering Desc
|
||||
await thead.getByText(nameColumn).click();
|
||||
|
||||
const responseOrderingDesc = await responsePromiseOrderingDesc;
|
||||
expect(responseOrderingDesc.ok()).toBeTruthy();
|
||||
const responseOrderingDesc = await responsePromiseOrderingDesc;
|
||||
expect(responseOrderingDesc.ok()).toBeTruthy();
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const textDocNameRow1Desc = await docNameRow1.textContent();
|
||||
const textDocNameRow2Desc = await docNameRow2.textContent();
|
||||
await expect(docNameRow1).toHaveText(/.*/);
|
||||
await expect(docNameRow2).toHaveText(/.*/);
|
||||
const textDocNameRow1Desc = await docNameRow1.textContent();
|
||||
const textDocNameRow2Desc = await docNameRow2.textContent();
|
||||
|
||||
expect(
|
||||
textDocNameRow1Desc &&
|
||||
textDocNameRow2Desc &&
|
||||
textDocNameRow1Desc.localeCompare(textDocNameRow2Desc, 'en', {
|
||||
caseFirst: 'false',
|
||||
ignorePunctuation: true,
|
||||
}) >= 0,
|
||||
).toBeTruthy();
|
||||
});
|
||||
},
|
||||
);
|
||||
expect(
|
||||
textDocNameRow1Desc &&
|
||||
textDocNameRow2Desc &&
|
||||
textDocNameRow1Desc.localeCompare(textDocNameRow2Desc, 'en', {
|
||||
caseFirst: 'false',
|
||||
ignorePunctuation: true,
|
||||
}) >= 0,
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('checks the pagination', async ({ page }) => {
|
||||
const responsePromisePage1 = page.waitForResponse(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||
import { goToGridDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -8,42 +8,57 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.describe('Doc Header', () => {
|
||||
test('it checks the element are correctly displayed', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
accesses: [
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super@owner.com',
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super@owner.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'admin',
|
||||
user: {
|
||||
email: 'super@admin.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super2@owner.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
abilities: {
|
||||
destroy: true, // Means owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: true,
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'admin',
|
||||
user: {
|
||||
email: 'super@admin.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
role: 'owner',
|
||||
user: {
|
||||
email: 'super2@owner.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
abilities: {
|
||||
destroy: true, // Means owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true,
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: true,
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
@@ -63,194 +78,4 @@ test.describe('Doc Header', () => {
|
||||
await expect(card.getByText('Your role: Owner')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('it updates the doc', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-update',
|
||||
browserName,
|
||||
1,
|
||||
true,
|
||||
);
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Update document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Update document "${randomDoc}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: 'Is it public ?' }),
|
||||
).toBeChecked();
|
||||
|
||||
await page.getByText('Document name').fill(`${randomDoc}-updated`);
|
||||
await page.getByText('Is it public ?').click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Validate the modification',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been updated.'),
|
||||
).toBeVisible();
|
||||
|
||||
const docTitle = await goToGridDoc(page, {
|
||||
title: `${randomDoc}-updated`,
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Update document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: 'Is it public ?' }),
|
||||
).not.toBeChecked();
|
||||
});
|
||||
|
||||
test('it deletes the doc', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Delete document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Deleting the document "${randomDoc}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Create a new document' }),
|
||||
).toBeVisible();
|
||||
|
||||
const row = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table')
|
||||
.getByRole('row')
|
||||
.filter({
|
||||
hasText: randomDoc,
|
||||
});
|
||||
|
||||
expect(await row.count()).toBe(0);
|
||||
});
|
||||
|
||||
test('it checks the options available if administrator', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true, // Means admin
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Update document' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks the options available if editor', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: true,
|
||||
partial_update: true, // Means editor
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Update document' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks the options available if reader', async ({ page }) => {
|
||||
await mockedDocument(page, {
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Update document' }),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,9 +111,6 @@ test.describe('Document create member', () => {
|
||||
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||
expect(
|
||||
responseCreateInvitation.request().headers()['content-language'],
|
||||
).toBe('en-us');
|
||||
|
||||
// Check user added
|
||||
await expect(
|
||||
@@ -121,18 +118,6 @@ test.describe('Document create member', () => {
|
||||
).toBeVisible();
|
||||
const responseAddUser = await responsePromiseAddUser;
|
||||
expect(responseAddUser.ok()).toBeTruthy();
|
||||
expect(responseAddUser.request().headers()['content-language']).toBe(
|
||||
'en-us',
|
||||
);
|
||||
|
||||
const listInvitation = page.getByLabel('List invitation card');
|
||||
await expect(listInvitation.locator('li').getByText(email)).toBeVisible();
|
||||
await expect(
|
||||
listInvitation.locator('li').getByText('Invited'),
|
||||
).toBeVisible();
|
||||
|
||||
const listMember = page.getByLabel('List members card');
|
||||
await expect(listMember.locator('li').getByText(user.email)).toBeVisible();
|
||||
});
|
||||
|
||||
test('it try to add twice the same user', async ({ page, browserName }) => {
|
||||
@@ -185,7 +170,7 @@ test.describe('Document create member', () => {
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
|
||||
const [email] = randomName('test@test.fr', browserName, 1);
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
@@ -206,105 +191,7 @@ test.describe('Document create member', () => {
|
||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
// Choose a role
|
||||
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||
await page.getByRole('option', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/invitations/') && response.status() === 400,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
await expect(
|
||||
page.getByText(`"${email}" is already invited to the document.`),
|
||||
).toBeVisible();
|
||||
const responseCreateInvitationFail =
|
||||
await responsePromiseCreateInvitationFail;
|
||||
expect(responseCreateInvitationFail.ok()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('The invitation endpoint get the language of the website', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'user-invitation', browserName, 1);
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.getByRole('combobox').getByText('EN').click();
|
||||
await header.getByRole('option', { name: 'FR' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Partager' }).click();
|
||||
|
||||
const inputSearch = page.getByLabel(
|
||||
/Trouver un membre à ajouter au document/,
|
||||
);
|
||||
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
// Choose a role
|
||||
await page.getByRole('combobox', { name: /Choisissez un rôle/ }).click();
|
||||
await page.getByRole('option', { name: 'Administrateur' }).click();
|
||||
|
||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/invitations/') && response.status() === 201,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Valider' }).click();
|
||||
|
||||
// Check invitation sent
|
||||
await expect(page.getByText(`Invitation envoyée à ${email}`)).toBeVisible();
|
||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||
expect(
|
||||
responseCreateInvitation.request().headers()['content-language'],
|
||||
).toBe('fr-fr');
|
||||
});
|
||||
|
||||
test('it manages invitation', async ({ page, browserName }) => {
|
||||
await createDoc(page, 'user-invitation', browserName, 1);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
// Choose a role
|
||||
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||
await page.getByRole('option', { name: 'Administrator' }).click();
|
||||
|
||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/invitations/') && response.status() === 201,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
|
||||
// Check invitation sent
|
||||
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||
|
||||
const listInvitation = page.getByLabel('List invitation card');
|
||||
const li = listInvitation.locator('li').filter({
|
||||
hasText: email,
|
||||
});
|
||||
await expect(li.getByText(email)).toBeVisible();
|
||||
|
||||
await li.getByRole('combobox', { name: /Role/ }).click();
|
||||
await li.getByRole('option', { name: 'Reader' }).click();
|
||||
await expect(page.getByText(`The role has been updated.`)).toBeVisible();
|
||||
await li.getByText('delete').click();
|
||||
await expect(
|
||||
page.getByText(`The invitation has been removed.`),
|
||||
).toBeVisible();
|
||||
await expect(listInvitation.locator('li').getByText(email)).toBeHidden();
|
||||
await expect(page.getByText('Loading...')).toBeHidden();
|
||||
await expect(page.getByRole('option', { name: email })).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Routing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('checks alias docs url with homepage', async ({ page }) => {
|
||||
await expect(page).toHaveURL('/');
|
||||
|
||||
@@ -16,9 +14,9 @@ test.describe('Doc Routing', () => {
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
|
||||
await page.goto('/docs/');
|
||||
await page.goto('/docs');
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/docs\/$/);
|
||||
await expect(page).toHaveURL(/\/docs$/);
|
||||
});
|
||||
|
||||
test('checks 404 on docs/[id] page', async ({ page }) => {
|
||||
@@ -35,16 +33,3 @@ test.describe('Doc Routing', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Doc Routing: Not loggued', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('checks redirect to a doc after login', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await page.goto('/docs/mocked-document-id/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
await expect(page).toHaveURL(/\/docs\/mocked-document-id\/$/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import pdf from 'pdf-parse';
|
||||
|
||||
import { createDoc, goToGridDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Tools', () => {
|
||||
test('it converts the doc to pdf with a template integrated', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Generate PDF',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
})
|
||||
.click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfText = (await pdf(pdfBuffer)).text;
|
||||
|
||||
expect(pdfText).toContain('Hello World'); // This is the doc text
|
||||
});
|
||||
|
||||
test('it converts the blocknote json in correct html for the pdf', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
let body = '';
|
||||
|
||||
await page.route('**/templates/*/generate-document/', async (route) => {
|
||||
const request = route.request();
|
||||
body = request.postDataJSON().body;
|
||||
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.locator('.bn-block-outer').last().fill('Hello World');
|
||||
await page.locator('.bn-block-outer').last().click();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.bn-block-outer').last().fill('Break');
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Generate PDF',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Download',
|
||||
})
|
||||
.click();
|
||||
|
||||
// Empty paragraph should be replaced by a <br/>
|
||||
expect(body).toContain('<br/><br/>');
|
||||
});
|
||||
|
||||
test('it updates the doc', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-update',
|
||||
browserName,
|
||||
1,
|
||||
true,
|
||||
);
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Update document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Update document "${randomDoc}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: 'Is it public ?' }),
|
||||
).toBeChecked();
|
||||
|
||||
await page.getByText('Document name').fill(`${randomDoc}-updated`);
|
||||
await page.getByText('Is it public ?').click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Validate the modification',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been updated.'),
|
||||
).toBeVisible();
|
||||
|
||||
const docTitle = await goToGridDoc(page, {
|
||||
title: `${randomDoc}-updated`,
|
||||
});
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Update document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('checkbox', { name: 'Is it public ?' }),
|
||||
).not.toBeChecked();
|
||||
});
|
||||
|
||||
test('it deletes the doc', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Delete document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Deleting the document "${randomDoc}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Create a new document' }),
|
||||
).toBeVisible();
|
||||
|
||||
const row = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table')
|
||||
.getByRole('row')
|
||||
.filter({
|
||||
hasText: randomDoc,
|
||||
});
|
||||
|
||||
expect(await row.count()).toBe(0);
|
||||
});
|
||||
|
||||
test('it checks the options available if administrator', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: true, // Means admin
|
||||
update: true,
|
||||
partial_update: true,
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Generate PDF' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Update document' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks the options available if editor', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: true,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: true,
|
||||
partial_update: true, // Means editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Generate PDF' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Update document' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks the options available if reader', async ({ page }) => {
|
||||
await page.route('**/documents/**/', async (route) => {
|
||||
const request = route.request();
|
||||
if (
|
||||
request.method().includes('GET') &&
|
||||
!request.url().includes('page=')
|
||||
) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
manage_accesses: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
is_public: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
|
||||
await page.getByLabel('Open the document options').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Generate PDF' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Update document' }),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Delete document' }),
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,11 @@ test.describe('Footer', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
const footer = page.locator('footer').first();
|
||||
|
||||
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
await expect(footer.getByAltText('Marianne Logo')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByAltText('Freedom Equality Fraternity Logo'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
footer.getByRole('link', { name: 'legifrance.gouv.fr' }),
|
||||
|
||||
@@ -2,15 +2,19 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.describe('Header', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Header', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
await expect(header.getByAltText('Marianne Logo')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
header.getByAltText('Freedom Equality Fraternity Logo'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(header.getByAltText('Docs Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "1.2.1",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
"install-playwright": "playwright install --with-deps",
|
||||
"install": "playwright install --with-deps",
|
||||
"test": "playwright test",
|
||||
"test:ui": "yarn test --ui",
|
||||
"test:ui::firefox": "yarn test:ui --project=firefox",
|
||||
"test:ui::webkit": "yarn test:ui --project=webkit",
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
"test:ui": "yarn test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.46.1",
|
||||
"@playwright/test": "1.45.0",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
@@ -20,7 +17,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"convert-stream": "1.0.2",
|
||||
"jsdom": "24.1.1",
|
||||
"pdf-parse": "1.1.1"
|
||||
"pdf-parse": "^1.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +63,5 @@ export default defineConfig({
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
locale: 'en-US',
|
||||
timezoneId: 'Europe/Paris',
|
||||
storageState: 'playwright/.auth/user-firefox.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
NEXT_PUBLIC_THEME=dsfr
|
||||
NEXT_PUBLIC_SIGNALING_URL=
|
||||
NEXT_PUBLIC_API_ORIGIN=
|
||||
@@ -1,2 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://test.jest
|
||||
NEXT_PUBLIC_THEME=test-theme
|
||||
|
||||
@@ -8,10 +8,6 @@ server {
|
||||
try_files $uri index.html $uri/ =404;
|
||||
}
|
||||
|
||||
location ~ ^/docs/(.*)/versions/(.*)/$ {
|
||||
error_page 404 /docs/[id]/versions/[versionId]/;
|
||||
}
|
||||
|
||||
location /docs/ {
|
||||
error_page 404 /docs/[id]/;
|
||||
}
|
||||
|
||||
@@ -39,12 +39,6 @@ const config = {
|
||||
xxs: '320px',
|
||||
xs: '480px',
|
||||
},
|
||||
logo: {
|
||||
src: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
alt: '',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
datagrid: {
|
||||
@@ -274,12 +268,6 @@ const config = {
|
||||
base: 'Marianne',
|
||||
},
|
||||
},
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
alert: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "1.2.1",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -18,33 +18,33 @@
|
||||
"@blocknote/core": "*",
|
||||
"@blocknote/mantine": "*",
|
||||
"@blocknote/react": "*",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@gouvfr-lasuite/integration": "1.0.1",
|
||||
"@openfun/cunningham-react": "2.9.3",
|
||||
"@tanstack/react-query": "5.51.24",
|
||||
"i18next": "23.14.0",
|
||||
"@tanstack/react-query": "5.48.0",
|
||||
"i18next": "23.11.5",
|
||||
"idb": "8.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"next": "14.2.5",
|
||||
"luxon": "3.4.4",
|
||||
"next": "14.2.4",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.3.1",
|
||||
"react-aria-components": "1.2.1",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.0.1",
|
||||
"react-i18next": "14.1.2",
|
||||
"react-select": "5.8.0",
|
||||
"styled-components": "6.1.12",
|
||||
"styled-components": "6.1.11",
|
||||
"y-webrtc": "10.3.0",
|
||||
"yjs": "*",
|
||||
"zustand": "4.5.5"
|
||||
"zustand": "4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.51.24",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.4.8",
|
||||
"@tanstack/react-query-devtools": "5.48.0",
|
||||
"@testing-library/dom": "10.2.0",
|
||||
"@testing-library/jest-dom": "6.4.6",
|
||||
"@testing-library/react": "16.0.0",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/lodash": "4.17.7",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "18.3.3",
|
||||
@@ -56,10 +56,10 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.3.3",
|
||||
"stylelint": "16.8.2",
|
||||
"prettier": "3.3.2",
|
||||
"stylelint": "16.6.1",
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"stylelint-prettier": "5.0.2",
|
||||
"stylelint-prettier": "5.0.0",
|
||||
"typescript": "*",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,241 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="30 24 260 146.5"
|
||||
>
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.st1 {
|
||||
fill: #000091;
|
||||
}
|
||||
.st2 {
|
||||
fill: #e1000f;
|
||||
}
|
||||
.st3 {
|
||||
fill: #9c9b9b;
|
||||
}
|
||||
</style>
|
||||
<g id="Calque_1">
|
||||
<path
|
||||
id="Devise_Républicaine_1_"
|
||||
d="M100.5,150.8c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6C99.1,151.8,99.9,150.8,100.5,150.8 M102,155.2
|
||||
h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4
|
||||
c0,1.3,0.6,2.1,1.6,2.1C99.8,158.2,101,157,102,155.2 M101.2,148.9l3-2.8v-0.3h-1.6l-1.9,3.2H101.2z M91.9,150.9h1.4l-2.3,6.2
|
||||
c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2
|
||||
l-1.8,0.2V150.9z M89.9,150.6c0.2-0.6-0.2-0.9-0.5-0.9c-1.2,0-2.7,1.1-3.3,2.7h0.4c0.4-0.6,1.1-1.2,1.7-1.3l-2.4,6.2
|
||||
c-0.2,0.6,0.2,0.9,0.5,0.9c1.2,0,2.6-1.1,3.2-2.7h-0.4c-0.4,0.6-1.1,1.2-1.7,1.3L89.9,150.6z M90.4,147.5c0.6,0,1-0.5,1-1
|
||||
c0-0.6-0.5-1-1-1c-0.6,0-1,0.5-1,1C89.3,147,89.8,147.5,90.4,147.5 M76.6,157c-0.3,0.7,0,1.2,0.7,1.2c0.4,0,0.6-0.1,0.8-0.6
|
||||
l1.7-4.4c0.8-0.9,2.3-1.9,3-1.9c0.5,0,0.4,0.4,0.1,0.9l-2.5,4.9c-0.2,0.5,0.1,1.1,0.6,1.1c1.2,0,2.7-1.1,3.3-2.7h-0.4
|
||||
c-0.4,0.6-1.1,1.2-1.7,1.3l2.2-4.4c0.3-0.6,0.4-1.1,0.4-1.5c0-0.7-0.4-1.2-1.2-1.2c-1.1,0-2.2,1.2-3.5,2.7v-1.2
|
||||
c0-0.8-0.3-1.6-1-1.6c-0.5,0-0.9,0.4-1.3,1v0.2c0.8,0,1.2,1.2,0.6,2.4L76.6,157z M76.6,151.6c0.3-1,0.1-1.9-0.6-1.9
|
||||
c-0.9,0-1.2,0.7-2.1,2.7v-1.2c0-0.8-0.3-1.6-1-1.6c-0.9,0-1.7,1.4-2.3,2.7H71c0.4-0.6,0.8-1,1.1-1c0.4,0,0.6,0.6,0,1.9l-1.7,3.7
|
||||
c-0.3,0.7,0,1.2,0.7,1.2c0.4,0,0.6-0.1,0.8-0.6l1.7-4.4c0.5-0.6,0.9-1.1,1.4-1.6H76.6z M67,150.8c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6
|
||||
C65.6,151.8,66.4,150.8,67,150.8 M68.5,155.2H68c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4
|
||||
c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4c0,1.3,0.6,2.1,1.6,2.1C66.3,158.2,67.5,157,68.5,155.2 M58.3,150.9h1.4l-2.3,6.2
|
||||
c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2
|
||||
l-1.8,0.2V150.9z M50.5,155.7c0-1.9,2.1-4.5,3.3-4.5c0.3,0,0.5,0,0.7,0.1l-1.2,3.3c-0.7,0.9-1.8,1.9-2.3,1.9
|
||||
C50.7,156.6,50.5,156.3,50.5,155.7 M57,149.3l-0.7,0l-0.7,0.7h-0.2c-3.6,0-6.6,4-6.6,6.9c0,0.8,0.5,1.3,1.3,1.3
|
||||
c0.9,0,1.8-1.3,2.8-2.7l0,0.5c-0.1,1.4,0.3,2.2,1.1,2.2c0.9,0,1.7-1.4,2.3-2.7h-0.4c-0.4,0.6-0.8,1-1.1,1c-0.3,0-0.6-0.6,0-1.9
|
||||
L57,149.3z M49.5,151.6c0.3-1,0.1-1.9-0.6-1.9c-0.9,0-1.2,0.7-2.1,2.7v-1.2c0-0.8-0.3-1.6-1-1.6c-0.9,0-1.7,1.4-2.3,2.7h0.4
|
||||
c0.4-0.6,0.8-1,1.1-1c0.4,0,0.6,0.6,0,1.9l-1.7,3.7c-0.3,0.7,0,1.2,0.7,1.2c0.4,0,0.6-0.1,0.8-0.6l1.7-4.4c0.5-0.6,0.9-1.1,1.4-1.6
|
||||
H49.5z M37.5,157.9l0.2-0.5c-2.1-0.4-2.3-0.4-1.5-2.6l0.8-2.3h2.3c1,0,1,0.4,0.9,1.5h0.6l1.4-3.8h-0.6c-0.5,0.9-0.9,1.5-2,1.5h-2.3
|
||||
l1.2-3.3c0.4-1,0.6-1.3,2-1.3h1c1.4,0,1.6,0.4,1.6,1.9h0.6l0.5-2.6h-8.6l-0.2,0.5c1.7,0.3,1.8,0.5,1,2.6l-1.9,5.1
|
||||
c-0.8,2.1-1.1,2.3-3,2.6l-0.1,0.5H37.5z M79.7,131.4c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6C78.3,132.3,79.1,131.4,79.7,131.4 M81.2,135.7
|
||||
h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4
|
||||
c0,1.3,0.6,2.1,1.6,2.1C79,138.7,80.2,137.6,81.2,135.7 M80.3,129.4l3-2.8v-0.3h-1.6l-1.9,3.2H80.3z M71,131.4h1.4l-2.3,6.2
|
||||
c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2
|
||||
l-1.8,0.2V131.4z M69.1,131.1c0.2-0.6-0.2-0.9-0.5-0.9c-1.2,0-2.7,1.1-3.3,2.7h0.4c0.4-0.6,1.1-1.2,1.7-1.3l-2.4,6.2
|
||||
c-0.2,0.6,0.2,0.9,0.5,0.9c1.2,0,2.6-1.1,3.2-2.7h-0.4c-0.4,0.6-1.1,1.2-1.7,1.3L69.1,131.1z M69.5,128c0.6,0,1-0.5,1-1
|
||||
c0-0.6-0.5-1-1-1c-0.6,0-1,0.5-1,1C68.5,127.6,68.9,128,69.5,128 M61.2,137.3l4.3-11.4l-0.1-0.2l-2.7,0.3v0.3l0.5,0.4
|
||||
c0.5,0.4,0.3,0.7-0.1,1.9l-3.4,8.9c-0.2,0.5,0.1,1.1,0.6,1.1c1.2,0,2.6-1.1,3.2-2.7h-0.4C62.7,136.6,61.8,137.2,61.2,137.3
|
||||
M53,136.3c0-1.9,2.1-4.5,3.3-4.5c0.3,0,0.5,0,0.7,0.1l-1.2,3.3c-0.7,0.9-1.8,1.9-2.3,1.9C53.1,137.1,53,136.8,53,136.3
|
||||
M59.5,129.9l-0.7,0l-0.7,0.7H58c-3.6,0-6.6,4-6.6,6.9c0,0.8,0.5,1.3,1.3,1.3c0.9,0,1.8-1.3,2.8-2.7l0,0.5
|
||||
c-0.1,1.4,0.3,2.2,1.1,2.2c0.9,0,1.7-1.4,2.3-2.7h-0.4c-0.4,0.6-0.8,1-1.1,1c-0.3,0-0.6-0.6,0-1.9L59.5,129.9z M43.7,140.2
|
||||
c0-0.8,0.8-1.3,1.9-1.8c0.4,0.2,0.9,0.4,1.7,0.6c1.2,0.4,1.6,0.5,1.6,0.9c0,0.8-1.3,1.3-3.1,1.3C44.4,141.3,43.7,141,43.7,140.2
|
||||
M46.9,135.2c-0.5,0-0.7-0.4-0.7-0.9c0-1.4,0.7-3.4,1.9-3.4c0.5,0,0.7,0.4,0.7,0.9C48.8,133.1,48,135.2,46.9,135.2 M50.3,139.4
|
||||
c0-1-0.9-1.4-2.3-1.8c-1.2-0.4-1.8-0.5-1.8-0.9c0-0.3,0.3-0.7,0.8-1c2-0.1,3.4-1.9,3.4-3.5c0-0.3-0.1-0.6-0.2-0.9h1.7l0.3-0.9h-2.7
|
||||
c-0.3-0.2-0.7-0.3-1.1-0.3c-2.2,0-3.6,1.9-3.6,3.4c0,1.2,0.7,1.9,1.7,2.1c-1,0.5-1.6,1-1.6,1.6c0,0.4,0.1,0.6,0.4,0.8
|
||||
c-2.3,0.7-3.3,1.5-3.3,2.6c0,1.1,1.4,1.5,3.1,1.5C47.9,142.3,50.3,140.7,50.3,139.4 M39.4,132.8c1,0,1,0.4,0.9,1.5h0.6l1.4-3.8
|
||||
h-0.6c-0.5,0.9-0.9,1.5-2,1.5h-2.3l1.1-3.1c0.4-1,0.6-1.2,2-1.2h1c1.4,0,1.6,0.4,1.6,1.8h0.6l0.5-2.6h-8.6l-0.2,0.5
|
||||
c1.7,0.3,1.8,0.5,1,2.6l-1.9,5.1c-0.8,2.1-1.1,2.3-3,2.6l-0.1,0.5H41l1.7-2.7H42c-1.1,1-2.5,2-4.4,2c-2.5,0-2.3-0.1-1.5-2.4
|
||||
l0.9-2.5H39.4z M40.6,126.2l3-2.1v-0.3h-1.8l-1.7,2.4H40.6z M78.7,111.9c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6
|
||||
C77.3,112.8,78,111.9,78.7,111.9 M80.2,116.2h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4
|
||||
c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4c0,1.3,0.6,2.1,1.6,2.1C77.9,119.2,79.2,118.1,80.2,116.2 M79.3,110l3-2.8v-0.3h-1.6
|
||||
l-1.9,3.2H79.3z M70.4,111.9h1.1l-2.3,6.2c-0.2,0.5,0.1,1.1,0.6,1.1c1.3,0,3.4-1.3,4.1-3h-0.4c-0.6,0.6-1.7,1.4-2.6,1.6l2.1-5.8
|
||||
h2.1l0.3-0.9h-2.1l0.8-2.2h-0.8l-1.5,2.2l-1.5,0.2V111.9z M69.2,112.6c0.3-1,0.1-1.9-0.6-1.9c-0.9,0-1.2,0.7-2.1,2.7v-1.2
|
||||
c0-0.8-0.3-1.6-1-1.6c-0.9,0-1.7,1.4-2.3,2.7h0.4c0.4-0.6,0.8-1,1.1-1c0.4,0,0.6,0.6,0,1.9l-1.7,3.7c-0.3,0.7,0,1.2,0.7,1.2
|
||||
c0.4,0,0.6-0.1,0.8-0.6l1.7-4.4c0.5-0.6,0.9-1.1,1.4-1.6H69.2z M59.7,111.9c0.6,0,1.1,0.4,0.8,1.5l-2.7,0.6
|
||||
C58.3,112.8,59.1,111.9,59.7,111.9 M61.2,116.2h-0.5c-0.7,0.8-1.4,1.4-2.1,1.4c-0.7,0-1.1-0.4-1.1-1.4c0-0.4,0-0.8,0.1-1.2l4.3-1.4
|
||||
c0.8-2-0.2-2.9-1.4-2.9c-2,0-4.4,3.4-4.4,6.4c0,1.3,0.6,2.1,1.6,2.1C59,119.2,60.2,118.1,61.2,116.2 M50.6,118c-0.4,0-1-0.4-1-0.7
|
||||
c0-0.1,0.2-0.6,0.4-1.2l0.7-1.9c0.7-0.9,1.9-1.8,2.5-1.8c0.4,0,0.7,0.2,0.7,0.8C53.9,114.9,52.3,118,50.6,118 M55.5,112.5
|
||||
c0-1.3-0.5-1.7-1.3-1.7c-1.1,0-2.1,1.2-3.1,2.6l2.6-6.8l-0.1-0.2l-2.7,0.3v0.3l0.5,0.4c0.5,0.4,0.3,0.8-0.1,1.9l-2.8,7.2
|
||||
c-0.2,0.5-0.5,1.2-0.5,1.3c0,0.7,1,1.4,1.9,1.4C52,119.2,55.5,115.4,55.5,112.5 M47,111.6c0.2-0.6-0.2-0.9-0.5-0.9
|
||||
c-1.2,0-2.7,1.1-3.3,2.7h0.4c0.4-0.6,1.1-1.2,1.7-1.3l-2.4,6.2c-0.2,0.6,0.2,0.9,0.5,0.9c1.2,0,2.6-1.1,3.2-2.7h-0.4
|
||||
c-0.4,0.6-1.1,1.2-1.7,1.3L47,111.6z M47.5,108.5c0.6,0,1-0.5,1-1c0-0.6-0.5-1-1-1c-0.6,0-1,0.5-1,1
|
||||
C46.4,108.1,46.9,108.5,47.5,108.5 M41.2,107.5h-5.7l-0.2,0.5c1.7,0.3,1.8,0.5,1,2.6l-1.8,5.1c-0.8,2.1-1.1,2.3-3,2.6l-0.1,0.5H40
|
||||
l1.9-3.3h-0.7c-1.1,1.2-2.3,2.6-4.2,2.6c-1.4,0-1.6-0.2-0.8-2.4l1.8-5.1c0.8-2.1,1.1-2.3,3-2.6L41.2,107.5z"
|
||||
/>
|
||||
<path
|
||||
d="M40.9,88.2c1,0,1.9-0.2,2.7-0.5c0.8-0.3,1.5-0.8,2.1-1.3v-3.5h-5.3v-3.8h9.4v8.8c-1,1.3-2.2,2.3-3.8,3.1s-3.3,1.2-5.2,1.2
|
||||
c-1.7,0-3.2-0.3-4.6-0.9c-1.4-0.6-2.6-1.4-3.6-2.4c-1-1-1.8-2.1-2.3-3.5c-0.6-1.3-0.8-2.7-0.8-4.2c0-1.5,0.3-2.9,0.8-4.2
|
||||
c0.5-1.3,1.3-2.5,2.2-3.5c1-1,2.1-1.8,3.5-2.4s2.8-0.9,4.5-0.9c1.9,0,3.5,0.4,5,1.2c1.5,0.8,2.7,1.8,3.7,3L46,77
|
||||
c-0.6-0.8-1.3-1.5-2.3-2.1s-2-0.8-3.1-0.8c-1,0-1.9,0.2-2.7,0.5c-0.8,0.4-1.5,0.9-2.1,1.5c-0.6,0.6-1,1.4-1.4,2.2
|
||||
c-0.3,0.9-0.5,1.8-0.5,2.8s0.2,1.9,0.5,2.8c0.3,0.9,0.8,1.6,1.4,2.2c0.6,0.6,1.4,1.1,2.2,1.5C39,88,39.9,88.2,40.9,88.2z M64,70.3
|
||||
c1.6,0,3.1,0.3,4.4,0.9s2.5,1.4,3.5,2.4c1,1,1.7,2.1,2.2,3.5c0.5,1.3,0.8,2.7,0.8,4.2c0,1.5-0.3,2.9-0.8,4.2
|
||||
c-0.5,1.3-1.3,2.5-2.2,3.5c-1,1-2.1,1.8-3.5,2.4s-2.8,0.9-4.4,0.9c-1.6,0-3.1-0.3-4.5-0.9s-2.5-1.4-3.5-2.4c-1-1-1.7-2.1-2.2-3.5
|
||||
c-0.5-1.3-0.8-2.7-0.8-4.2c0-1.5,0.3-2.9,0.8-4.2c0.5-1.3,1.3-2.5,2.2-3.5c1-1,2.1-1.8,3.5-2.4S62.4,70.3,64,70.3z M64,88.2
|
||||
c1,0,1.9-0.2,2.7-0.5c0.8-0.4,1.5-0.9,2.1-1.5c0.6-0.6,1-1.4,1.4-2.2s0.5-1.8,0.5-2.8s-0.2-1.9-0.5-2.8c-0.3-0.9-0.8-1.6-1.4-2.2
|
||||
c-0.6-0.6-1.3-1.1-2.1-1.5c-0.8-0.4-1.7-0.5-2.7-0.5c-1,0-1.9,0.2-2.7,0.5c-0.8,0.4-1.5,0.9-2.1,1.5c-0.6,0.6-1,1.4-1.4,2.2
|
||||
c-0.3,0.9-0.5,1.8-0.5,2.8s0.2,1.9,0.5,2.8c0.3,0.9,0.8,1.6,1.4,2.2c0.6,0.6,1.3,1.1,2.1,1.5C62.1,88,63,88.2,64,88.2z M91.1,83.8
|
||||
V70.9h4.2v12.6c0,2.7-0.8,4.8-2.3,6.4c-1.5,1.5-3.5,2.3-6.1,2.3c-2.6,0-4.6-0.8-6.1-2.3s-2.2-3.7-2.2-6.4V70.9h4.2v12.9
|
||||
c0,1.4,0.4,2.5,1.1,3.2c0.7,0.8,1.8,1.2,3.1,1.2c1.3,0,2.3-0.4,3-1.2C90.8,86.3,91.1,85.2,91.1,83.8z M97.9,70.9h4.5l6.1,16.1
|
||||
l6.1-16.1h4.5l-7.8,20.7h-5.5L97.9,70.9z M122.2,91.5V70.9h12v3.6h-7.8v4.8h6.7v3.6h-6.7V88h7.8v3.6H122.2z M139.1,91.5V70.9h6.3
|
||||
c2.3,0,4.1,0.6,5.4,1.7c1.3,1.1,2,2.6,2,4.5c0,1.2-0.3,2.3-0.9,3.2c-0.6,0.9-1.4,1.6-2.4,2.1l6.5,9.1h-5l-5.5-8.3h-2.2v8.3H139.1z
|
||||
M145.7,74.4h-2.4v5.2h2.4c0.9,0,1.6-0.2,2.1-0.7c0.5-0.5,0.7-1.1,0.7-1.9c0-0.8-0.2-1.4-0.7-1.9C147.2,74.7,146.6,74.4,145.7,74.4
|
||||
z M158.6,91.5V70.9h5.4l9.2,14.8V70.9h4.2v20.7H172l-9.2-14.8v14.8H158.6z M183.1,91.5V70.9h12v3.6h-7.8v4.8h6.7v3.6h-6.7V88h7.8
|
||||
v3.6H183.1z M200,91.5V70.9h5.3l5,8.5l5-8.5h5.3v20.7h-4.2V76.8l-4.6,7.6h-3l-4.6-7.6v14.7H200z M226.3,91.5V70.9h12v3.6h-7.8v4.8
|
||||
h6.7v3.6h-6.7V88h7.8v3.6H226.3z M243.2,91.5V70.9h5.4l9.2,14.8V70.9h4.2v20.7h-5.4l-9.2-14.8v14.8H243.2z M265.7,74.7v-3.8h16.9
|
||||
v3.8h-6.4v16.8H272V74.7H265.7z"
|
||||
/>
|
||||
<g id="Marianne">
|
||||
<path
|
||||
id="Fond_2_"
|
||||
class="st0"
|
||||
d="M63.6,53.4c0.3-0.3,0.6-0.6,0.9-1h0c0.6-0.6,1.1-1.3,1.8-1.8c0.2-0.2,0.4-0.3,0.6-0.5
|
||||
c0.1-0.1,0.1-0.2,0.1-0.2c-0.3,0.1-0.4,0.3-0.7,0.4c-0.1,0-0.1-0.1-0.1-0.1c0.2-0.1,0.4-0.3,0.6-0.4c0,0,0,0,0,0
|
||||
c-0.1,0-0.1-0.1-0.1-0.1c-0.7-0.1-1.2,0.4-1.7,0.8c-0.1,0.1-0.2-0.1-0.3-0.1c-0.8,0.3-1.4,1-2.2,1.3v-0.1c-0.3,0.1-0.6,0.3-1,0.4
|
||||
c-0.5,0.1-0.9,0.1-1.3,0.1c-0.6,0.1-1.3,0.2-1.9,0.3c0,0,0,0-0.1,0c-0.3,0.1-0.7,0.2-1,0.4c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1
|
||||
c-0.1,0.1-0.2,0.2-0.4,0.3c-0.3,0.2-0.6,0.5-0.9,0.7c0,0-0.1,0-0.1,0c-0.3,0.3-0.6,0.6-0.9,0.8c0,0-0.1,0-0.2,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0-0.1c0-0.1,0.1-0.2,0.1-0.2c0.1-0.1,0.1-0.2,0.2-0.2c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0-0.1,0-0.1c0,0,0,0-0.1,0
|
||||
c0.3-0.3,0.6-0.5,0.9-0.7v0c0,0-0.1,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-0.2,0.1-0.3,0.2
|
||||
c-0.1,0.1-0.2,0.4-0.4,0.3c0,0-0.1,0-0.1,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0-0.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0-0.1
|
||||
c0.1-0.1,0.2-0.2,0.4-0.3h0c0.2-0.1,0.4-0.2,0.6-0.3c0,0,0.1-0.1,0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.4c0,0-0.1,0-0.1,0
|
||||
c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1,0,0.1,0.1c1.7-1.3,4.1-1,6.1-1.7c0.2-0.1,0.3-0.2,0.5-0.3
|
||||
c0.3-0.1,0.5-0.4,0.8-0.5c0.4-0.3,0.7-0.7,0.9-1.2c0-0.1-0.1-0.1-0.1-0.1c-0.7,0.8-1.5,1.3-2.4,1.8c-1.1,0.6-2.4,0.5-3.5,0.6
|
||||
c0.1-0.1,0.2-0.1,0.3-0.1c0-0.2,0.1-0.2,0.2-0.3H59c0.1,0,0.1-0.1,0.1-0.1c0.1,0,0.3-0.1,0.2-0.1c-0.2-0.2-0.5,0.2-0.8,0
|
||||
c0.1-0.1,0.1-0.3,0.2-0.3h0.2c0-0.1,0.1-0.2,0.1-0.2c0.8-0.5,1.6-0.9,2.3-1.3c-0.2,0-0.3,0.2-0.4,0.1c0.1,0,0-0.2,0.1-0.2
|
||||
c0.6-0.2,1.1-0.5,1.7-0.7c-0.2,0-0.4,0.2-0.6,0c0.1-0.1,0.2-0.2,0.3-0.2v-0.2c0-0.1,0.1-0.1,0.1-0.1c-0.1,0-0.1-0.1-0.1-0.1
|
||||
c0.1-0.1,0.2-0.1,0.3-0.2c-0.1,0-0.2,0-0.2-0.1c0.2-0.2,0.4-0.3,0.7-0.3c-0.1-0.1-0.2,0-0.2-0.1c0-0.1,0.1-0.1,0.1-0.1H63
|
||||
c-0.1-0.1-0.1-0.2-0.1-0.2c0.3-0.4,0.3-0.9,0.5-1.3c-0.1,0-0.1,0-0.1-0.1c-0.5,0.6-1.4,0.8-2.2,1h-0.3c-0.3,0.1-0.6,0.1-0.9-0.1
|
||||
c-0.2-0.1-0.3-0.3-0.5-0.4c-0.4-0.3-0.9-0.5-1.3-0.6c-1.3-0.4-2.7-0.6-4.1-0.6c0.6-0.3,1.2-0.3,1.9-0.5c0.9-0.3,1.8-0.6,2.7-0.5
|
||||
c-0.2-0.1-0.4,0-0.5,0c-0.8-0.1-1.5,0.2-2.3,0.3c-0.5,0.1-1,0.3-1.6,0.4c-0.3,0.1-0.5,0.4-0.9,0.4v-0.2c0.5-0.6,1.2-1.3,2-1.3
|
||||
c1-0.2,1.9,0,2.8,0.1c0.7,0.1,1.3,0.2,2,0.4c0.3,0,0.3,0.4,0.5,0.5c0.3,0.1,0.6,0,1,0.2c0-0.1-0.1-0.2,0-0.3
|
||||
c0.2-0.2,0.5,0.1,0.7-0.1c0.4-0.3-0.4-0.7-0.6-1.1c0-0.1,0.1-0.1,0.1-0.1c0.4,0.4,0.7,0.8,1.3,1.1c0.3,0.1,0.9,0.3,0.8-0.1
|
||||
c-0.3-0.6-0.8-1.1-1.2-1.6v-0.2c-0.1,0-0.1-0.1-0.2-0.1v-0.2c-0.2-0.1-0.2-0.3-0.3-0.5c-0.2-0.3-0.1-0.6-0.2-1
|
||||
C62.1,39.3,62,39,62,38.7c-0.2-0.9-0.4-1.7-0.5-2.6c-0.1-1,0.6-1.8,1.1-2.7c0.4-0.6,0.8-1.3,1.5-1.7c0.2-0.6,0.6-1.2,1-1.7
|
||||
c0.4-0.5,1.1-0.8,1.7-1.1c0.7-0.3,1.4-0.5,1.4-0.5l10.7,0c0,0,0.1,0,0.3,0.1c0.2,0.1,0.6,0.3,0.8,0.4c0.4,0.2,0.8,0.5,1,0.9
|
||||
c0.1,0.2,0.3,0.5,0.2,0.7c-0.1,0.3-0.2,0.7-0.4,0.8c-0.3,0.2-0.7,0.2-1.1,0.1c-0.2,0-0.4-0.1-0.6-0.1c0.8,0.3,1.6,0.7,2.1,1.4
|
||||
c0.1,0.1,0.3,0.2,0.5,0.2c0.1,0,0.1,0.1,0.1,0.2c-0.1,0.1-0.2,0.2-0.2,0.3h0.2c0.3-0.1,0.2-0.6,0.6-0.5c0.3,0.2,0.4,0.5,0.2,0.8
|
||||
c-0.2,0.2-0.4,0.4-0.6,0.5c-0.1,0.1-0.1,0.3,0,0.4c0.2,0.2,0.2,0.4,0.3,0.6c0.2,0.4,0.2,0.8,0.4,1.2c0.2,0.8,0.4,1.6,0.4,2.4
|
||||
c0,0.4-0.2,0.8-0.1,1.2c0.1,0.4,0.4,0.8,0.6,1.1c0.2,0.3,0.4,0.5,0.6,0.9c0.3,0.5,0.9,1.1,0.6,1.7c-0.2,0.4-0.8,0.3-1.1,0.5
|
||||
c-0.3,0.3-0.1,0.7,0.1,1c0.3,0.5-0.3,0.8-0.7,1c0.1,0.2,0.3,0.1,0.4,0.2c0.1,0.3,0.3,0.4,0.2,0.7c-0.2,0.3-0.9,0.5-0.5,1
|
||||
c0.2,0.4,0.1,0.8-0.1,1.2c-0.2,0.5-0.6,0.7-1,0.8c-0.3,0.1-0.7,0.1-1,0.1c-0.1-0.1-0.2-0.1-0.3-0.1c-0.9-0.1-1.8-0.4-2.7-0.4
|
||||
c-0.3,0.1-0.5,0.1-0.7,0.2c-0.2,0.2-0.5,0.4-0.6,0.6c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0,0,0,0,0.1
|
||||
c-0.2,0.2-0.3,0.4-0.4,0.6c0,0,0,0,0,0c0,0,0,0.1,0,0.1c-0.2,0.3-0.3,0.7-0.4,1c-0.4,1.2-0.2,2.3,0.1,2.5c0.1,0.1,1.8,0.6,2.9,1.1
|
||||
c0.6,0.2,0.9,0.4,1.3,0.6l-22,0c1-0.7,2-1.1,3.5-1.8C61.6,54.6,63.1,53.9,63.6,53.4 M55.4,49.5c-0.1,0-0.3,0.1-0.3-0.1
|
||||
c0.1-0.3,0.4-0.3,0.6-0.4c0.1-0.1,0.3-0.2,0.4-0.1c0.1,0.2,0.3,0.1,0.4,0.2C56.2,49.5,55.8,49.4,55.4,49.5 M47.2,48.3
|
||||
c0,0-0.1-0.1-0.1-0.1c0.7-0.9,1.2-1.8,1.7-2.7c0.7-0.4,1.3-0.9,1.8-1.5c0.9-1,1.9-1.8,3-2.4c0.4-0.2,1-0.1,1.4,0.1
|
||||
c-0.2,0.2-0.4,0.2-0.6,0.3c-0.1,0-0.1,0-0.2-0.1c0.1-0.1,0.1-0.1,0.1-0.2c-0.5,0.6-1.3,0.9-1.7,1.6c-0.3,0.5-0.5,1.2-1.2,1.4
|
||||
c-0.2,0.1,0.1-0.2-0.1-0.1C49.6,45.7,48.5,46.9,47.2,48.3 M51.6,44.8c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.1,0.2-0.2,0.2
|
||||
c-0.1,0-0.1,0-0.1-0.1c0.1-0.2,0.2-0.4,0.4-0.5C51.6,44.7,51.6,44.8,51.6,44.8 M54.1,52.8c0,0.1-0.1,0.1-0.1,0.2
|
||||
c0.1,0,0.1,0,0.1,0.1c-0.1,0.1-0.2,0.2-0.4,0.3c0,0,0,0-0.1,0c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.4,0-0.3-0.1
|
||||
c0.1-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.2-0.1,0.2-0.2c0,0,0.1-0.1,0.1-0.1C53.9,52.8,54.2,52.7,54.1,52.8 M53.2,52.4
|
||||
C53.2,52.4,53.1,52.4,53.2,52.4c-0.2,0.2-0.4,0.3-0.6,0.4c-0.2,0.1-0.5,0.2-0.7,0.3c0,0,0,0,0,0c0,0-0.1,0-0.1,0
|
||||
c-0.2,0.1-0.4,0.3-0.5,0.4c0,0,0,0-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.2,0.2-0.3,0.3c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1
|
||||
l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1l0,0c0,0,0,0,0,0c0.1-0.1,0.1-0.1,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0.1-0.1
|
||||
c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
|
||||
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0-0.1
|
||||
c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0,0,0,0c-0.1,0-0.1,0.1-0.2,0.2c-0.1,0-0.2,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0
|
||||
c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.5-0.5,1.4-0.5,2.1-0.8
|
||||
c0.3-0.1,0.6,0.1,0.9,0c0.2,0,0.3,0,0.5,0.1C54,51.8,53.6,52.1,53.2,52.4 M54.3,48.7c-0.1-0.1,0.2,0,0.2-0.1H54
|
||||
c-0.1,0-0.1-0.1-0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.2c-0.4,0.1-0.7,0.4-1.1,0.5c-0.6,0.2-1.1,0.7-1.7,0.9c-0.1,0-0.1-0.1-0.1-0.1
|
||||
c0.1-0.2,0.3-0.2,0.4-0.4c0-0.1,0-0.1-0.1-0.1c0.4-0.6,1-0.9,1.6-1.4v-0.2c0.2-0.2,0.4-0.3,0.5-0.6c0.1-0.2,0.3-0.4,0.5-0.5
|
||||
c-0.1-0.1-0.2-0.1-0.2-0.2c-0.2,0-0.4,0.1-0.6-0.1c0.1-0.1,0.2-0.2,0.3-0.2c0,0-0.1,0-0.1-0.1c-0.1-0.1,0.1-0.2,0.3-0.3
|
||||
c0.2-0.1,0.5-0.1,0.6-0.2c-0.4-0.1-0.8,0.1-1.2-0.1c0.3-0.7,0.7-1.3,1.3-1.6c0.1,0,0.2,0,0.2,0.1c0,0.3-0.2,0.5-0.4,0.5
|
||||
c0.4,0.1,0.9,0.1,1.3,0.3c-0.1,0.1-0.2,0.1-0.2,0.1c0.3,0.2,0.6,0.1,0.9,0.3c-0.2,0.2-0.3,0-0.5,0c1.7,0.5,3.4,0.9,4.8,1.9
|
||||
c-1.2,0.6-2.4,0.9-3.7,1.1c-0.2,0-0.3,0-0.4-0.1c0,0.1,0,0.2-0.1,0.2c-0.2,0-0.4,0-0.5,0.1C54.7,48.8,54.4,48.8,54.3,48.7"
|
||||
/>
|
||||
<path
|
||||
id="Rouge_1_"
|
||||
class="st1"
|
||||
d="M63.6,53.4c0.3-0.3,0.6-0.6,0.9-1h0c0.6-0.6,1.1-1.3,1.8-1.8c0.2-0.2,0.4-0.3,0.6-0.5
|
||||
c0.1-0.1,0.1-0.2,0.1-0.2c-0.3,0.1-0.4,0.3-0.7,0.4c-0.1,0-0.1-0.1-0.1-0.1c0.2-0.1,0.4-0.3,0.6-0.4c0,0,0,0,0,0
|
||||
c-0.1,0-0.1-0.1-0.1-0.1c-0.7-0.1-1.2,0.4-1.7,0.8c-0.1,0.1-0.2-0.1-0.3-0.1c-0.8,0.3-1.4,1-2.2,1.3v-0.1c-0.3,0.1-0.6,0.3-1,0.4
|
||||
c-0.5,0.1-0.9,0.1-1.3,0.1c-0.6,0.1-1.3,0.2-1.9,0.3c0,0,0,0-0.1,0c-0.3,0.1-0.7,0.2-1,0.4c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1
|
||||
c-0.1,0.1-0.2,0.2-0.4,0.3c-0.3,0.2-0.6,0.5-0.9,0.7c0,0-0.1,0-0.1,0c-0.3,0.3-0.6,0.6-0.9,0.8c0,0-0.1,0-0.2,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0-0.1c0-0.1,0.1-0.2,0.1-0.2c0.1-0.1,0.1-0.2,0.2-0.2c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0-0.1,0-0.1c0,0,0,0-0.1,0
|
||||
c0.3-0.3,0.6-0.5,0.9-0.7v0c0,0-0.1,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-0.2,0.1-0.3,0.2
|
||||
c-0.1,0.1-0.2,0.4-0.4,0.3c0,0-0.1,0-0.1,0c0,0,0,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0-0.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0-0.1
|
||||
c0.1-0.1,0.2-0.2,0.4-0.3h0c0.2-0.1,0.4-0.2,0.6-0.3c0,0,0.1-0.1,0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.4c0,0-0.1,0-0.1,0
|
||||
c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1,0,0.1,0.1c1.7-1.3,4.1-1,6.1-1.7c0.2-0.1,0.3-0.2,0.5-0.3
|
||||
c0.3-0.1,0.5-0.4,0.8-0.5c0.4-0.3,0.7-0.7,0.9-1.2c0-0.1-0.1-0.1-0.1-0.1c-0.7,0.8-1.5,1.3-2.4,1.8c-1.1,0.6-2.4,0.5-3.5,0.6
|
||||
c0.1-0.1,0.2-0.1,0.3-0.1c0-0.2,0.1-0.2,0.2-0.3H59c0.1,0,0.1-0.1,0.1-0.1c0.1,0,0.3-0.1,0.2-0.1c-0.2-0.2-0.5,0.2-0.8,0
|
||||
c0.1-0.1,0.1-0.3,0.2-0.3h0.2c0-0.1,0.1-0.2,0.1-0.2c0.8-0.5,1.6-0.9,2.3-1.3c-0.2,0-0.3,0.2-0.4,0.1c0.1,0,0-0.2,0.1-0.2
|
||||
c0.6-0.2,1.1-0.5,1.7-0.7c-0.2,0-0.4,0.2-0.6,0c0.1-0.1,0.2-0.2,0.3-0.2v-0.2c0-0.1,0.1-0.1,0.1-0.1c-0.1,0-0.1-0.1-0.1-0.1
|
||||
c0.1-0.1,0.2-0.1,0.3-0.2c-0.1,0-0.2,0-0.2-0.1c0.2-0.2,0.4-0.3,0.7-0.3c-0.1-0.1-0.2,0-0.2-0.1c0-0.1,0.1-0.1,0.1-0.1H63
|
||||
c-0.1-0.1-0.1-0.2-0.1-0.2c0.3-0.4,0.3-0.9,0.5-1.3c-0.1,0-0.1,0-0.1-0.1c-0.5,0.6-1.4,0.8-2.2,1h-0.3c-0.3,0.1-0.6,0.1-0.9-0.1
|
||||
c-0.2-0.1-0.3-0.3-0.5-0.4c-0.4-0.3-0.9-0.5-1.3-0.6c-1.3-0.4-2.7-0.6-4.1-0.6c0.6-0.3,1.2-0.3,1.9-0.5c0.9-0.3,1.8-0.6,2.7-0.5
|
||||
c-0.2-0.1-0.4,0-0.5,0c-0.8-0.1-1.5,0.2-2.3,0.3c-0.5,0.1-1,0.3-1.6,0.4c-0.3,0.1-0.5,0.4-0.9,0.4V44c0.5-0.6,1.2-1.3,2-1.3
|
||||
c1-0.2,1.9,0,2.8,0.1c0.7,0.1,1.3,0.2,2,0.4c0.3,0,0.3,0.4,0.5,0.5c0.3,0.1,0.6,0,1,0.2c0-0.1-0.1-0.2,0-0.3
|
||||
c0.2-0.2,0.5,0.1,0.7-0.1c0.4-0.3-0.4-0.7-0.6-1.1c0-0.1,0.1-0.1,0.1-0.1c0.4,0.4,0.7,0.8,1.3,1.1c0.3,0.1,0.9,0.3,0.8-0.1
|
||||
c-0.3-0.6-0.8-1.1-1.2-1.6v-0.2c-0.1,0-0.1-0.1-0.2-0.1v-0.2c-0.2-0.1-0.2-0.3-0.3-0.5c-0.2-0.3-0.1-0.6-0.2-1
|
||||
C62.1,39.3,62,39,62,38.7c-0.2-0.9-0.4-1.7-0.5-2.6c-0.1-1,0.6-1.8,1.1-2.7c0.4-0.6,0.8-1.3,1.5-1.7c0.2-0.6,0.6-1.2,1-1.7
|
||||
c0.4-0.5,1.1-0.8,1.7-1.1c0.7-0.3,1.4-0.5,1.4-0.5H31.4v28.3h26c1-0.7,2-1.1,3.5-1.8C61.6,54.6,63.1,53.9,63.6,53.4 M55.4,49.5
|
||||
c-0.1,0-0.3,0.1-0.3-0.1c0.1-0.3,0.4-0.3,0.6-0.4c0.1-0.1,0.3-0.2,0.4-0.1c0.1,0.2,0.3,0.1,0.4,0.2C56.2,49.5,55.8,49.4,55.4,49.5
|
||||
M47.2,48.3c0,0-0.1-0.1-0.1-0.1c0.7-0.9,1.2-1.8,1.7-2.7c0.7-0.4,1.3-0.9,1.8-1.5c0.9-1,1.9-1.8,3-2.4c0.4-0.2,1-0.1,1.4,0.1
|
||||
c-0.2,0.2-0.4,0.2-0.6,0.3c-0.1,0-0.1,0-0.2-0.1c0.1-0.1,0.1-0.1,0.1-0.2c-0.5,0.6-1.3,0.9-1.7,1.6c-0.3,0.5-0.5,1.2-1.2,1.4
|
||||
c-0.2,0.1,0.1-0.2-0.1-0.1C49.7,45.7,48.5,46.9,47.2,48.3 M51.6,44.8c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.1,0.2-0.2,0.2
|
||||
c-0.1,0-0.1,0-0.1-0.1c0.1-0.2,0.2-0.4,0.4-0.5C51.6,44.7,51.6,44.8,51.6,44.8 M54.1,52.8c0,0.1-0.1,0.1-0.1,0.2
|
||||
c0.1,0,0.1,0,0.1,0.1c-0.1,0.1-0.2,0.2-0.4,0.3c0,0,0,0-0.1,0c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.4,0-0.3-0.1
|
||||
c0.1-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.2-0.1,0.2-0.2c0,0,0.1-0.1,0.1-0.1C53.9,52.8,54.2,52.7,54.1,52.8 M53.2,52.4
|
||||
C53.2,52.4,53.2,52.4,53.2,52.4c-0.2,0.2-0.4,0.3-0.6,0.4c-0.2,0.1-0.5,0.2-0.7,0.3c0,0,0,0,0,0c0,0-0.1,0-0.1,0
|
||||
c-0.2,0.1-0.4,0.3-0.5,0.4c0,0,0,0-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.2,0.2-0.3,0.3c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0.1
|
||||
l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0-0.1l0,0c0,0,0,0,0,0c0.1-0.1,0.1-0.1,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0.1-0.1
|
||||
c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.2c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
|
||||
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0-0.1,0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0-0.1c0,0,0,0,0,0c0,0,0-0.1,0-0.1
|
||||
c0,0,0,0,0,0c0.1-0.1,0.1-0.2,0.2-0.3c0,0,0,0,0,0c-0.1,0-0.1,0.1-0.2,0.2c-0.1,0-0.2,0-0.1-0.1c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0
|
||||
c0.1-0.1,0.2-0.2,0.3-0.3c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0,0c0,0,0.1-0.1,0.1-0.1c0,0,0,0,0,0c0.5-0.5,1.4-0.5,2.1-0.8
|
||||
c0.3-0.1,0.6,0.1,0.9,0c0.2,0,0.3,0,0.5,0.1C54,51.8,53.6,52.1,53.2,52.4 M54.3,48.7c-0.1-0.1,0.2,0,0.2-0.1H54
|
||||
c-0.1,0-0.1-0.1-0.1-0.1c-0.3,0.1-0.6,0.2-0.9,0.2c-0.4,0.1-0.7,0.4-1.1,0.5c-0.6,0.2-1.1,0.7-1.7,0.9c-0.1,0-0.1-0.1-0.1-0.1
|
||||
c0.1-0.2,0.3-0.2,0.4-0.4c0-0.1,0-0.1-0.1-0.1c0.4-0.6,1-0.9,1.6-1.4v-0.2c0.2-0.2,0.4-0.3,0.5-0.6c0.1-0.2,0.3-0.4,0.5-0.5
|
||||
c-0.1-0.1-0.2-0.1-0.2-0.2c-0.2,0-0.4,0.1-0.6-0.1c0.1-0.1,0.2-0.2,0.3-0.2c0,0-0.1,0-0.1-0.1c-0.1-0.1,0.1-0.2,0.3-0.3
|
||||
c0.2-0.1,0.5-0.1,0.6-0.2c-0.4-0.1-0.8,0.1-1.2-0.1c0.3-0.7,0.7-1.3,1.3-1.6c0.1,0,0.2,0,0.2,0.1c0,0.3-0.2,0.5-0.4,0.5
|
||||
c0.4,0.1,0.9,0.1,1.3,0.3c-0.1,0.1-0.2,0.1-0.2,0.1c0.3,0.2,0.6,0.1,0.9,0.3c-0.2,0.2-0.3,0-0.5,0c1.7,0.5,3.4,0.9,4.8,1.9
|
||||
c-1.2,0.6-2.4,0.9-3.7,1.1c-0.2,0-0.3,0-0.4-0.1c0,0.1,0,0.2-0.1,0.2c-0.2,0-0.4,0-0.5,0.1C54.7,48.8,54.4,48.8,54.3,48.7"
|
||||
/>
|
||||
<path
|
||||
id="Bleu_1_"
|
||||
class="st2"
|
||||
d="M109.3,28.4H78.9c0,0,0.1,0,0.3,0.1c0.2,0.1,0.6,0.3,0.8,0.4c0.4,0.2,0.8,0.5,1,0.9
|
||||
c0.1,0.2,0.3,0.5,0.2,0.7c-0.1,0.3-0.2,0.7-0.4,0.8c-0.3,0.2-0.7,0.2-1.1,0.1c-0.2,0-0.4-0.1-0.6-0.1c0.8,0.3,1.6,0.7,2.1,1.4
|
||||
c0.1,0.1,0.3,0.2,0.5,0.2c0.1,0,0.1,0.1,0.1,0.2c-0.1,0.1-0.2,0.2-0.2,0.3h0.2c0.3-0.1,0.2-0.6,0.6-0.5c0.3,0.2,0.4,0.5,0.2,0.8
|
||||
c-0.2,0.2-0.4,0.4-0.6,0.5c-0.1,0.1-0.1,0.3,0,0.4c0.2,0.2,0.2,0.4,0.3,0.6c0.2,0.4,0.2,0.8,0.4,1.2c0.2,0.8,0.4,1.6,0.4,2.4
|
||||
c0,0.4-0.2,0.8-0.1,1.2c0.1,0.4,0.4,0.8,0.6,1.1c0.2,0.3,0.4,0.5,0.6,0.9c0.3,0.5,0.9,1.1,0.6,1.7c-0.2,0.4-0.8,0.3-1.1,0.5
|
||||
c-0.3,0.3-0.1,0.7,0.1,1c0.3,0.5-0.3,0.8-0.7,1c0.1,0.2,0.3,0.1,0.4,0.2c0.1,0.3,0.3,0.4,0.2,0.7c-0.2,0.3-0.9,0.5-0.5,1
|
||||
c0.2,0.4,0.1,0.8-0.1,1.2c-0.2,0.5-0.6,0.7-1,0.8c-0.3,0.1-0.7,0.1-1,0.1c-0.1-0.1-0.2-0.1-0.3-0.1c-0.9-0.1-1.8-0.4-2.7-0.4
|
||||
c-0.3,0.1-0.5,0.1-0.7,0.2c-0.2,0.2-0.5,0.4-0.6,0.6c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c0,0,0,0.1-0.1,0.1c0,0,0,0,0,0.1
|
||||
c-0.2,0.2-0.3,0.4-0.4,0.6c0,0,0,0,0,0c0,0,0,0.1,0,0.1c-0.2,0.3-0.3,0.7-0.4,1c-0.4,1.2-0.2,2.3,0.1,2.5c0.1,0.1,1.8,0.6,2.9,1.1
|
||||
c0.6,0.2,0.9,0.4,1.3,0.6h29.9V28.4z"
|
||||
/>
|
||||
<path
|
||||
id="Yeux_1_"
|
||||
class="st3"
|
||||
d="M80.7,38.7c0.2,0.1,0.5,0.1,0.5,0.2c-0.1,0.4-0.7,0.5-1.1,1H80c-0.2,0.1-0.1,0.4-0.3,0.4
|
||||
c-0.2-0.1-0.3,0-0.5,0.1c0.2,0.2,0.5,0.4,0.8,0.3c0.1,0,0.2,0.1,0.2,0.2c0,0,0.1,0,0.1-0.1c0.1,0,0.1,0,0.1,0.1V41
|
||||
c-0.2,0.2-0.4,0.1-0.6,0.2c0.4,0.1,0.9,0.1,1.2,0c0.3-0.1,0-0.6,0.2-0.9c-0.1,0,0-0.2-0.1-0.2c0.1-0.1,0.2-0.3,0.3-0.3
|
||||
c0.1,0,0.3-0.1,0.3-0.2c0-0.1-0.2-0.2-0.2-0.3c0.3-0.2,0.6-0.5,0.5-0.9c-0.1-0.2-0.5-0.2-0.8-0.3c-0.3-0.1-0.6,0-0.9,0.1
|
||||
c-0.3,0-0.5,0.2-0.8,0.2c-0.4,0.1-0.7,0.3-1,0.5c0.4-0.2,0.8-0.2,1.2-0.3C80.1,38.7,80.3,38.6,80.7,38.7"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
InfiniteData,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError } from './APIError';
|
||||
import { APIList } from './types';
|
||||
|
||||
export type UseQueryOptionsAPI<Q> = UseQueryOptions<Q, APIError, Q>;
|
||||
export type DefinedInitialDataInfiniteOptionsAPI<Q> =
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
Q,
|
||||
APIError,
|
||||
InfiniteData<Q>,
|
||||
QueryKey,
|
||||
number
|
||||
>;
|
||||
|
||||
/**
|
||||
* @param param Used for infinite scroll pagination
|
||||
* @param queryConfig
|
||||
* @returns
|
||||
*/
|
||||
export const useAPIInfiniteQuery = <T, Q extends { next?: APIList<Q>['next'] }>(
|
||||
key: string,
|
||||
api: (props: T & { page: number }) => Promise<Q>,
|
||||
param: T,
|
||||
queryConfig?: DefinedInitialDataInfiniteOptionsAPI<Q>,
|
||||
) => {
|
||||
return useInfiniteQuery<Q, APIError, InfiniteData<Q>, QueryKey, number>({
|
||||
initialPageParam: 1,
|
||||
queryKey: [key, param],
|
||||
queryFn: ({ pageParam }) =>
|
||||
api({
|
||||
...param,
|
||||
page: pageParam,
|
||||
}),
|
||||
getNextPageParam(lastPage, allPages) {
|
||||
return lastPage.next ? allPages.length + 1 : undefined;
|
||||
},
|
||||
...queryConfig,
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './APIError';
|
||||
export * from './fetchApi';
|
||||
export * from './helpers';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,42 @@
|
||||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { default as IconDevise } from '@/assets/icons/icon-devise.svg?url';
|
||||
import { default as IconMarianne } from '@/assets/icons/icon-marianne.svg?url';
|
||||
|
||||
import { Box } from './Box';
|
||||
import { Text, TextType } from './Text';
|
||||
|
||||
interface LogoGouvProps {
|
||||
imagesWidth?: number;
|
||||
textProps?: TextType;
|
||||
}
|
||||
|
||||
const LogoGouv = ({ imagesWidth, textProps }: LogoGouvProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<Image
|
||||
priority
|
||||
src={IconMarianne}
|
||||
alt={t('Marianne Logo')}
|
||||
width={imagesWidth}
|
||||
/>
|
||||
</Box>
|
||||
<Text $weight="bold" $size="1.3rem" {...textProps}>
|
||||
Gouvernement
|
||||
</Text>
|
||||
<Image
|
||||
width={imagesWidth}
|
||||
priority
|
||||
src={IconDevise}
|
||||
alt={t('Freedom Equality Fraternity Logo')}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoGouv;
|
||||
@@ -17,6 +17,7 @@ export interface TextProps extends BoxProps {
|
||||
$isMaterialIcon?: boolean;
|
||||
$weight?: CSSProperties['fontWeight'];
|
||||
$textAlign?: CSSProperties['textAlign'];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
$size?: TextSizes | (string & {});
|
||||
$theme?:
|
||||
| 'primary'
|
||||
|
||||
@@ -5,6 +5,5 @@ export * from './DropButton';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
export * from './Link';
|
||||
export * from './SideModal';
|
||||
export * from './Text';
|
||||
export * from './TextErrors';
|
||||
|
||||
@@ -4,7 +4,6 @@ import { PropsWithChildren, useEffect } from 'react';
|
||||
import { Box } from '@/components';
|
||||
|
||||
import { useAuthStore } from './useAuthStore';
|
||||
|
||||
export const Auth = ({ children }: PropsWithChildren) => {
|
||||
const { authenticated, initAuth } = useAuthStore();
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
|
||||
@@ -3,7 +3,6 @@ import { create } from 'zustand';
|
||||
import { baseApiUrl } from '@/core/conf';
|
||||
|
||||
import { User, getMe } from './api';
|
||||
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
|
||||
interface AuthStore {
|
||||
authenticated: boolean;
|
||||
@@ -24,26 +23,9 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
||||
initAuth: () => {
|
||||
getMe()
|
||||
.then((data: User) => {
|
||||
// If a path is stored in the local storage, we redirect to it
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
window.location.replace(path_auth);
|
||||
return;
|
||||
}
|
||||
|
||||
set({ authenticated: true, userData: data });
|
||||
})
|
||||
.catch(() => {
|
||||
// If we try to access a specific page and we are not authenticated
|
||||
// we store the path in the local storage to redirect to it after login
|
||||
if (window.location.pathname !== '/') {
|
||||
localStorage.setItem(
|
||||
PATH_AUTH_LOCAL_STORAGE,
|
||||
window.location.pathname,
|
||||
);
|
||||
}
|
||||
|
||||
window.location.replace(new URL('authenticate/', baseApiUrl()).href);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import useCunninghamTheme from '../useCunninghamTheme';
|
||||
|
||||
describe('<useCunninghamTheme />', () => {
|
||||
it('has the theme from NEXT_PUBLIC_THEME', () => {
|
||||
const { theme } = useCunninghamTheme.getState();
|
||||
|
||||
expect(theme).toBe('test-theme');
|
||||
});
|
||||
|
||||
it('has the dsfr logo correctly set', () => {
|
||||
const { themeTokens, setTheme } = useCunninghamTheme.getState();
|
||||
setTheme('dsfr');
|
||||
const logo = themeTokens().logo;
|
||||
expect(logo?.src).toBe('/assets/logo-gouv.svg');
|
||||
expect(logo?.widthHeader).toBe('110px');
|
||||
expect(logo?.widthFooter).toBe('220px');
|
||||
});
|
||||
});
|
||||
@@ -125,10 +125,6 @@
|
||||
--c--theme--breakpoints--xl: 1200px;
|
||||
--c--theme--breakpoints--xxl: 1400px;
|
||||
--c--theme--breakpoints--xxs: 320px;
|
||||
--c--theme--logo--src: ;
|
||||
--c--theme--logo--widthheader: ;
|
||||
--c--theme--logo--widthfooter: ;
|
||||
--c--theme--logo--alt: ;
|
||||
--c--components--datagrid--header--weight: var(
|
||||
--c--theme--font--weights--extrabold
|
||||
);
|
||||
@@ -410,10 +406,6 @@
|
||||
--c--theme--colors--danger-900: #3a1c1c;
|
||||
--c--theme--font--families--accent: marianne;
|
||||
--c--theme--font--families--base: marianne;
|
||||
--c--theme--logo--src: /assets/logo-gouv.svg;
|
||||
--c--theme--logo--widthHeader: 110px;
|
||||
--c--theme--logo--widthFooter: 220px;
|
||||
--c--theme--logo--alt: gouvernement logo;
|
||||
--c--components--alert--border-radius: 0;
|
||||
--c--components--button--medium-height: 48px;
|
||||
--c--components--button--border-radius: 4px;
|
||||
|
||||
@@ -146,7 +146,6 @@ export const tokens = {
|
||||
xxl: '1400px',
|
||||
xxs: '320px',
|
||||
},
|
||||
logo: { src: '', widthHeader: '', widthFooter: '', alt: '' },
|
||||
},
|
||||
components: {
|
||||
datagrid: {
|
||||
@@ -408,12 +407,6 @@ export const tokens = {
|
||||
'danger-900': '#3a1c1c',
|
||||
},
|
||||
font: { families: { accent: 'Marianne', base: 'Marianne' } },
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
alert: { 'border-radius': '0' },
|
||||
|
||||