Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9790824ce1 | |||
| ccadd9567a | |||
| 2c9758eb30 | |||
| 0f71a3fcfa | |||
| 839b78a6d0 | |||
| 69f2641159 | |||
| e5de5a4345 | |||
| c2d6e60ae8 | |||
| ff832239d3 | |||
| a60399883b | |||
| 12d32fe933 | |||
| 038d868d29 | |||
| bfde526361 | |||
| 132da0837f | |||
| 164ed885ca | |||
| f5a35c66bb | |||
| 8100422776 | |||
| f48e385d3d | |||
| b5895bf297 | |||
| 0fb0c71cfa | |||
| a0fea64630 | |||
| de922e1c04 | |||
| 8007c45a35 | |||
| 3d370e5714 | |||
| 03953f0fe7 | |||
| 67f4ddeef7 | |||
| 6f4b4bb7d3 | |||
| c27e6c1b06 | |||
| d2888d0b9d | |||
| 6b8af1f9ec | |||
| 35852dff0b | |||
| be93598b2d | |||
| 7d3fd25c61 | |||
| e3fe647e5b | |||
| 8f5c413482 | |||
| a66231d72e | |||
| 2e0b6b2a2a | |||
| edffbbaf99 | |||
| a393cd4df0 | |||
| e26b806965 | |||
| aef19cfddd | |||
| b7bd7b7772 | |||
| 43ba36af61 | |||
| b4e4ba3516 | |||
| 9c19b22a66 | |||
| d87c278cce | |||
| 287a5b3bff | |||
| cfb979a411 | |||
| d588ae847f | |||
| 08a39c7ebd | |||
| aef5dd51fc | |||
| bf10fbd25f | |||
| f5948959f2 | |||
| a0644efb45 | |||
| b4e8db9050 | |||
| 96bfbfabec | |||
| c2aa5be8ff | |||
| 3b77e1254e | |||
| b436a418ff | |||
| 9fdf87b100 | |||
| 6943f2158a | |||
| 872cc8c110 | |||
| c03e1ffe8b | |||
| 5454da5c70 | |||
| 5b3c91f6dd | |||
| f1ac0954ed | |||
| fc80c2ec2a | |||
| 8ba64322be | |||
| ae9555284d | |||
| 14a91a5698 | |||
| 9f6dac53d4 | |||
| 5ba35dbc1d | |||
| 62098ec753 | |||
| 9e318f88be | |||
| 63b565e720 | |||
| e7fe153a81 | |||
| 8e0850b13e | |||
| 8fb18ed186 | |||
| bec6ff3d22 | |||
| 0571aaabce | |||
| e4bed84343 | |||
| 37f02893ed | |||
| 4e4e2e23e3 | |||
| b68015852c | |||
| 2f79d03acc | |||
| 8762a7bc92 | |||
| 589b0eab32 | |||
| 44f4a37814 | |||
| 6c76f7faab | |||
| 201d776d1b | |||
| e7f2317b41 | |||
| 206c7f11ae | |||
| 615f92ded3 | |||
| da3ad91283 | |||
| 197b16c5d0 | |||
| cce0331b68 | |||
| 380ac0cbcf | |||
| 1779b7bab4 | |||
| 7b5698a370 | |||
| f039b2aa3b | |||
| 8531e0dd08 | |||
| e2586b1d8e | |||
| 327a5f2ed4 | |||
| 1e62320803 | |||
| ff7341e544 | |||
| 038c3d6207 | |||
| 2b67d0bd26 | |||
| d30ac8ee4e | |||
| c05b98ce0d | |||
| 3d594a99d5 | |||
| b47e60d6ec | |||
| f40f0d35cb | |||
| f1227ed037 | |||
| dbd4477e9a | |||
| b4b308bda9 | |||
| 2b456c231f | |||
| 72bb079f10 | |||
| 06af320d61 | |||
| 926fe37e85 | |||
| 51325df7d9 | |||
| 0727fe0273 | |||
| d0d0b44f57 | |||
| 6e46b686ab | |||
| 38eabe2fc8 | |||
| effa77dc10 | |||
| d5d374c311 | |||
| dd33c99532 | |||
| f4146fbe4e | |||
| dce040f4c3 | |||
| 3a68c75feb | |||
| 29577c0419 | |||
| 2813b2ca27 | |||
| ef2f4d3250 | |||
| 996cea49b4 | |||
| eb2936f48b |
@@ -0,0 +1,52 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'preprod'
|
||||
- 'production'
|
||||
|
||||
|
||||
jobs:
|
||||
notify-argocd:
|
||||
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: "impress,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: Call argocd github webhook
|
||||
run: |
|
||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL
|
||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_PRODUCTION_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_PRODUCTION_WEBHOOK_URL
|
||||
|
||||
start-test-on-preprod:
|
||||
needs:
|
||||
- notify-argocd
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.ref, 'refs/tags/preprod')
|
||||
steps:
|
||||
-
|
||||
name: Debug
|
||||
run: |
|
||||
echo "Start test when preprod is ready"
|
||||
@@ -19,20 +19,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
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: "impress,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: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/impress-backend
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: .github/workflows/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -52,20 +63,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
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: "impress,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: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/impress-frontend
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: .github/workflows/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -86,20 +108,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
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: "impress,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: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/impress-y-webrtc-signaling
|
||||
-
|
||||
name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: .github/workflows/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -125,13 +158,24 @@ jobs:
|
||||
github.event_name != 'pull_request'
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
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: "impress,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: .github/workflows/secrets.enc.env
|
||||
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
-
|
||||
name: Call argocd github webhook
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
name: impress Workflow
|
||||
name: Frontend Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
install-front:
|
||||
@@ -21,13 +19,13 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -38,7 +36,7 @@ jobs:
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
build-front:
|
||||
@@ -52,7 +50,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
- name: Build CI App
|
||||
@@ -75,7 +73,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
- name: Test App
|
||||
@@ -92,9 +90,9 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
|
||||
- name: Check linting
|
||||
run: cd src/frontend/ && yarn lint
|
||||
|
||||
@@ -105,18 +103,25 @@ jobs:
|
||||
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'
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
- name: Restore the build cache
|
||||
@@ -125,15 +130,25 @@ jobs:
|
||||
with:
|
||||
path: src/frontend/apps/impress/out/
|
||||
key: build-front-${{ github.run_id }}
|
||||
|
||||
- name: Build and Start Docker Servers
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
COMPOSE_DOCKER_CLI_BUILD: 1
|
||||
|
||||
- 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: |
|
||||
docker-compose build --pull --build-arg BUILDKIT_INLINE_CACHE=1
|
||||
make run
|
||||
|
||||
|
||||
- name: Apply DRF migrations
|
||||
run: |
|
||||
make migrate
|
||||
@@ -154,4 +169,3 @@ jobs:
|
||||
name: playwright-report
|
||||
path: src/frontend/apps/e2e/report/
|
||||
retention-days: 7
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
name: impress Workflow
|
||||
name: Main Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
lint-git:
|
||||
@@ -18,7 +16,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
run: git log
|
||||
- name: Enforce absence of print statements in code
|
||||
@@ -34,11 +32,16 @@ jobs:
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git whatchanged --name-only --pretty="" origin..HEAD | grep CHANGELOG
|
||||
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -60,22 +63,38 @@ jobs:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
- name: Install node dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build mails
|
||||
run: yarn build
|
||||
- name: Persist mails' templates
|
||||
uses: actions/upload-artifact@v4
|
||||
node-version: "18"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
name: mails-templates
|
||||
path: src/backend/core/templates/mail
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
- name: Install yarn
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install node dependencies
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build mails
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn build
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -88,16 +107,15 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: "3.10"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: ~/.local/bin/ruff format impress --diff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: ~/.local/bin/ruff check impress
|
||||
run: ~/.local/bin/ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: ~/.local/bin/pylint impress
|
||||
|
||||
run: ~/.local/bin/pylint .
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -143,11 +161,12 @@ jobs:
|
||||
sudo mkdir -p /data/media && \
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
- name: Download mails' templates
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
name: mails-templates
|
||||
path: src/backend/core/templates/mail
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
- name: Start Minio
|
||||
run: |
|
||||
@@ -171,7 +190,7 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
@@ -190,8 +209,26 @@ jobs:
|
||||
i18n-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
-
|
||||
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: |
|
||||
@@ -201,7 +238,7 @@ jobs:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install development dependencies
|
||||
working-directory: src/backend
|
||||
@@ -210,17 +247,11 @@ jobs:
|
||||
- name: Generate the translation base file
|
||||
run: ~/.local/bin/django-admin makemessages --keep-pot --all
|
||||
|
||||
- name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: .github/workflows/secrets.enc.env
|
||||
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'yarn'
|
||||
node-version: "18.x"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: src/frontend/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -239,4 +270,3 @@ jobs:
|
||||
-v "${{ github.workspace }}:/app" \
|
||||
crowdin/cli:3.16.0 \
|
||||
crowdin upload sources -c /app/crowdin/config.yml
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
SOPS_PRIVATE=ENC[AES256_GCM,data:53ysyQ9gq2PnAQKNjOL+e+Bu5SQIuOguz8Bo5CpqbpYsF0AmV1WsOutckdClbu6ApqV3m9/Cj1FJ30+L/+j05pvcpqMeehPQwGQ=,iv:VMuML9IXiEqKY9jp+ny76jnQHmewq2rqdBy1wYpZkSI=,tag:aAZgwiWDg1AG4wk3f2Fq4w==,type:str]
|
||||
CROWDIN_API_TOKEN=ENC[AES256_GCM,data:bwh38oLDH4BpI2H+7oUjtVizyrYvVJ6Av4ECTnyPPthMz6DCaYQn55RXp8rQDgJj4bPRls+JcRVC94zYIjgpkDsbbcqHr620KQKHQHMgoOQ=,iv:hydpwWtCiOkhBpAYyNwDzSjhjfdUJcKX7YX3/PXteN0=,tag:eQLniL5XxkNs5yThUuQHyw==,type:str]
|
||||
CROWDIN_BASE_PATH=ENC[AES256_GCM,data:LJZE454A6qg=,iv:yIjGACBJSX3S9g7PAHRFn074xL94fHvMLcTKzFYwkwo=,tag:1Z8+UbeDOvTxR80b95KumQ==,type:str]
|
||||
CROWDIN_PROJECT_ID=ENC[AES256_GCM,data:THoNz661,iv:Ixd0D9tnpEWd2yqZui1HJQEO/h7YsAC1R9Vjj8OHBjA=,tag:wfDHhzaXLD3NwY5zDj24RA==,type:str]
|
||||
DOCKER_HUB_PASSWORD=ENC[AES256_GCM,data:jj92OOVMtsagOXQ=,iv:r/u8M70PspZMFCbi8a3FvuCDtWt+9YGArPNHZRpHA+k=,tag:WM3vzVkuQZVdHa3wh4satg==,type:str]
|
||||
DOCKER_HUB_USER=ENC[AES256_GCM,data:btdtLdLApQ==,iv:y1o2zwyzusBS6JiQSEtZwS2zctISo+UgAFhyZ53vbKQ=,tag:ZLkMJydgjMBmbbKq979z7g==,type:str]
|
||||
ARGOCD_WEBHOOK_URL=ENC[AES256_GCM,data:0TnoZv7vQI+8MZ/7EITx0Mvez66G6BcCzw+Mic+NH2qh0BdZBH8ynkYBleKw9V6TbucgHasa7duL,iv:GeE5tSpjAndThrXrzz8Dk6ah9Bxv6JQCJmKAfsToDi0=,tag:O2pIhA0ge1xygIv0izSMxg==,type:str]
|
||||
ARGOCD_WEBHOOK_SECRET=ENC[AES256_GCM,data:SrdWdV24lGztyUnFXeOYGAhqTErRFakIm7hBw8n4NKW6ll6AgeZKY6w7pbvgFknQ+NlRd/EK7bYk7CZtPDGU6zM=,iv:IkWxnTWrvzWwNh4RSt3N7iPHA7K7jkzSHa4CHptxxvU=,tag:XFVYBRsuDF/La1/8ADQ2jw==,type:str]
|
||||
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBESDdJSzBaaVlEbHRjSlIy\naVoyY2l6RVVqVXhOekV4NHdHQjV6Q0IzSEJNCk9JY3BFQ2tFWXBZVFMyWTJUYjdz\nMVdheTd4cjhFREl5MmNncmlobVNyUUUKLS0tIEg1MHBsV2FoRkFlN2JoNlFuTFFS\nNG5yUXZpQVY4Z1FGZmVLUjBqQWhSQTgKfT7hD5LVWg2NOrdyeIiVt6BX/4dt6fpN\nyydn2U0yxMg9fUZ7KkixAaWpChL3rvi3OWM07h6EdsznTwehLiMFTw==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_0__map_recipient=age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJa3EzbDJBeHcrUE44SXpM\ndlVheHdxc2I4ellwcHlUQkhWL2NiMFpBYUd3CmJxZUZhL0tZVkViQTZFRVRFbndC\nd2ljZUJxczZqSmdqcXlzYkZlZ2t4MTgKLS0tIFFmbHE1NXpOYlRnb2wzSTRVbTQ4\nMDhTNzN6WHovMXFhek5pbXZlMW1PdEkKJlydhV9Es+y2ngMwZMGnuF+JnEV1TGZH\nkWoBHxTSA7WEgwnhGaCe7kuzXrvv2ikrV1Ww7sN4wmqfCGC2sdkPBQ==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_1__map_recipient=age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzZEpFZU5maklnN1N4S0kw\nRGFNYzBGR2tFT2d5VzlRYU9NUWVvZld0REQ4CldvTlFtK0RFU0tuNjVhNEM4VzlC\nWjJhUEZVY0l0T05yNVBabXNEdndlbVkKLS0tIGxxdEROcWxpSHczMkN0dkdicnVZ\nT1BXR1hSa2l1SXdYS3RoWWh6NGdWSHcKZJd6HYESjLomY7/S9+eCCN4cFXERipNl\nWtOVZXlufN5BMxX8n8TlKS34oD1t6/CMaZZdmp2SHHslipA+CGRZ5g==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_2__map_recipient=age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||
sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzakNpcGkzWlp6NWt1NFU0\ncmhFek1DTU5YS1MyYzRoOGJ2RXdjRU5WcEZBCjN5eUp6WVh0YmdNMzdHTUNJTVZM\ncHZTY3pxbHd0TmhSWmQyVndZS1JjZ00KLS0tIFNxYjZXRHBKbjNxVitQaGlKQVh0\ncHAwbzFyL3hUVmN2dVNQaklIcXZKQjgKr4IO6BoTFO7Km9V/h8tF3UNRCGUXymIw\nnQGL0ZDyIQw7MMBQQ2mksYPSBTFmaejbSd29UkhVnYFuCjJ+LVmX1w==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_3__map_recipient=age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpS0hNdDk2Lys4Rk9nVlV1\nWHVwOHcxT3RmZkVSMWh6L0M1bGRrNEt3c1M0CmdseVlqaFZYZjd6KzI0ejdDSG55\nNkFlMGpiOFhMZWtKYkVodGpmUWRsMjgKLS0tIG5ZbVFadk5XVlREZFFEcWNiSDhw\nVnh5b3BURGU4bCtQQzR3b3hxcXdGSlEKBw7E/umovQnucE4oYeuoHFlEtYBMVXPL\n6YjZzBpBxJ+4kZpMvqsXzowQ7ZDEods9pEcuJmHqxrRpLeOrYrykTA==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_4__map_recipient=age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
sops_lastmodified=2024-04-03T15:36:15Z
|
||||
sops_mac=ENC[AES256_GCM,data:1v44C4K4YjV1m7tZKRgj8SiDamdD+L4p3TVwwOl6+05KCOh2uH2ohH+5MH7MTFL489oqaadpjBQfELSJ8h/4fN5MT6+Trbtk5QFLv4moLZx1tSCE1Tuam2cicFem2mlOrxb0pK/tU1qzCLvZke3yvFmiJEa+92u7y96hXM4VR6Y=,iv:23T3Tl5DvRH8zvef7ftbr5GWk+YFfLCzZ/eEzqjMKXY=,tag:TIch+2911w5qleXo55zM0w==,type:str]
|
||||
sops_unencrypted_suffix=_unencrypted
|
||||
sops_version=3.8.1
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "secrets"]
|
||||
path = secrets
|
||||
url = ../secrets
|
||||
+8
-12
@@ -1,13 +1,9 @@
|
||||
creation_rules:
|
||||
# Here we have
|
||||
# - Jacques key-id: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
# - github-repo key-id: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
# - Anthony Le-Courric key-id: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||
# - Antoine Lebaud key-id: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
# - argocd key-id: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
- age:
|
||||
age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x,
|
||||
age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7,
|
||||
age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg,
|
||||
age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3,
|
||||
age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
- path_regex: ./*
|
||||
key_groups:
|
||||
- age:
|
||||
- age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x # jacques
|
||||
- age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7 # github-repo
|
||||
- age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg # Anthony Le-Courric
|
||||
- age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3 # Antoine Lebaud
|
||||
|
||||
|
||||
+58
-3
@@ -8,13 +8,68 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.1.0] - 2024-07-15
|
||||
|
||||
## Added
|
||||
|
||||
- 🤡(demo) generate dummy documents on dev users #120
|
||||
- ✨(frontend) create side modal component #134
|
||||
- ✨(frontend) Doc grid actions (update / delete) #136
|
||||
- ✨(frontend) Doc editor header information #137
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️(frontend) replace docs panel with docs grid #120
|
||||
- ♻️(frontend) create a doc from a modal #132
|
||||
- ♻️(frontend) manage members from the share modal #140
|
||||
|
||||
## [1.0.0] - 2024-07-02
|
||||
|
||||
## Added
|
||||
|
||||
- 🛂(frontend) Manage the document's right (#75)
|
||||
- ✨(frontend) Update document (#68)
|
||||
- ✨(frontend) Remove document (#68)
|
||||
- 🐳(docker) dockerize dev frontend (#63)
|
||||
- 👔(backend) list users with email filtering (#79)
|
||||
- ✨(frontend) add user to a document (#52)
|
||||
- ✨(frontend) invite user to a document (#52)
|
||||
- 🛂(frontend) manage members (update role / list / remove) (#81)
|
||||
- ✨(frontend) offline mode (#88)
|
||||
- 🌐(frontend) translate cgu (#83)
|
||||
- ✨(service-worker) offline doc management (#94)
|
||||
- ⚗️(frontend) Add beta tag on logo (#121)
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️(frontend) Change site from Impress to Docs (#76)
|
||||
- ✨(frontend) Generate PDF from a modal (#68)
|
||||
- 🔧(helm) sticky session by request_uri for signaling server (#78)
|
||||
- ♻️(frontend) change logo (#84)
|
||||
- ♻️(frontend) pdf has title doc (#84)
|
||||
- ⚡️(e2e) unique login between tests (#80)
|
||||
- ⚡️(CI) improve e2e job (#86)
|
||||
- ♻️(frontend) improve the error and message info ui (#93)
|
||||
- ✏️(frontend) change all occurences of pad to doc (#99)
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛(frontend) Fix the break line when generate PDF (#84)
|
||||
|
||||
## Delete
|
||||
|
||||
- 💚(CI) Remove trigger workflow on push tags on CI (#68)
|
||||
- 🔥(frontend) Remove coming soon page (#121)
|
||||
|
||||
## [0.1.0] - 2024-05-24
|
||||
|
||||
## Added
|
||||
|
||||
- Coming Soon page (#67)
|
||||
- Impress, project to manage your documents easily and collaboratively.
|
||||
- ✨(frontend) Coming Soon page (#67)
|
||||
- 🚀 Impress, project to manage your documents easily and collaboratively.
|
||||
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/impress/compare/v0.1.0...main
|
||||
[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
|
||||
@@ -82,13 +82,12 @@ bootstrap: \
|
||||
data/static \
|
||||
create-env-files \
|
||||
build \
|
||||
run \
|
||||
run-frontend-dev \
|
||||
migrate \
|
||||
demo \
|
||||
back-i18n-compile \
|
||||
mails-install \
|
||||
mails-build \
|
||||
install-front-impress
|
||||
mails-build
|
||||
.PHONY: bootstrap
|
||||
|
||||
# -- Docker/compose
|
||||
@@ -105,10 +104,7 @@ logs: ## display app-dev logs (follow mode)
|
||||
.PHONY: logs
|
||||
|
||||
run: ## start the wsgi (production) and development server
|
||||
@$(COMPOSE) up --force-recreate -d nginx
|
||||
@$(COMPOSE) up --force-recreate -d app-dev
|
||||
@$(COMPOSE) up --force-recreate -d celery-dev
|
||||
@$(COMPOSE) up --force-recreate -d keycloak
|
||||
@$(COMPOSE) up --force-recreate -d y-webrtc-signaling
|
||||
@echo "Wait for postgresql to be up..."
|
||||
@$(WAIT_DB)
|
||||
@@ -301,13 +297,9 @@ help:
|
||||
.PHONY: help
|
||||
|
||||
# Front
|
||||
install-front-impress: ## Install the frontend dependencies of app Impress
|
||||
cd $(PATH_FRONT_IMPRESS) && yarn
|
||||
.PHONY: install-front-impress
|
||||
|
||||
run-front-impress: ## Start app Impress
|
||||
cd $(PATH_FRONT_IMPRESS) && yarn dev
|
||||
.PHONY: run-front-impress
|
||||
run-frontend-dev: ## Install and run the frontend dev
|
||||
@$(COMPOSE) up --force-recreate -d frontend-dev
|
||||
.PHONY: run-frontend-dev
|
||||
|
||||
frontend-i18n-extract: ## Extract the frontend translation inside a json to be used for crowdin
|
||||
cd $(PATH_FRONT) && yarn i18n:extract
|
||||
|
||||
@@ -31,10 +31,7 @@ The easiest way to start working on the project is to use GNU Make:
|
||||
$ make bootstrap FLUSH_ARGS='--no-input'
|
||||
```
|
||||
|
||||
Then you can run the following command to start the project in development mode:
|
||||
```bash
|
||||
$ make run-front-impress
|
||||
```
|
||||
Then you can access to the project in development mode by going to http://localhost:3000.
|
||||
You will be prompted to log in, the default credentials are:
|
||||
```bash
|
||||
username: impress
|
||||
@@ -52,7 +49,7 @@ Your Docker services should now be up and running 🎉
|
||||
Note that if you need to run them afterwards, you can use the eponym Make rule:
|
||||
|
||||
```bash
|
||||
$ make run
|
||||
$ make run-frontend-dev
|
||||
```
|
||||
|
||||
### Adding content
|
||||
|
||||
+3
-3
@@ -21,10 +21,10 @@ docker_build(
|
||||
'localhost:5001/impress-y-webrtc-signaling:latest',
|
||||
context='..',
|
||||
dockerfile='../src/frontend/Dockerfile',
|
||||
only=['./src/frontend/', './docker/', './dockerignore'],
|
||||
only=['./src/frontend/', './docker/', './.dockerignore'],
|
||||
target = 'y-webrtc-signaling',
|
||||
live_update=[
|
||||
sync('../src/frontend', '/home/frontend'),
|
||||
sync('../src/frontend/apps/y-webrtc-signaling/src', '/home/frontend/apps/y-webrtc-signaling/src'),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ docker_build(
|
||||
'localhost:5001/impress-frontend:latest',
|
||||
context='..',
|
||||
dockerfile='../src/frontend/Dockerfile',
|
||||
only=['./src/frontend', './docker', './dockerignore'],
|
||||
only=['./src/frontend', './docker', './.dockerignore'],
|
||||
target = 'impress',
|
||||
live_update=[
|
||||
sync('../src/frontend', '/home/frontend'),
|
||||
|
||||
Regular → Executable
+15
-1
@@ -65,6 +65,7 @@ services:
|
||||
- mailcatcher
|
||||
- redis
|
||||
- createbuckets
|
||||
- nginx
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
@@ -118,7 +119,6 @@ services:
|
||||
volumes:
|
||||
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
depends_on:
|
||||
- app
|
||||
- keycloak
|
||||
|
||||
dockerize:
|
||||
@@ -155,6 +155,20 @@ services:
|
||||
- /home/frontend/apps/y-webrtc-signaling/node_modules/
|
||||
- /home/frontend/apps/y-webrtc-signaling/dist/
|
||||
|
||||
frontend-dev:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/Dockerfile
|
||||
target: impress-dev
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./src/frontend/apps/impress:/home/frontend/apps/impress
|
||||
- /home/frontend/node_modules/
|
||||
depends_on:
|
||||
- y-webrtc-signaling
|
||||
- celery-dev
|
||||
|
||||
kc_postgresql:
|
||||
image: postgres:14.3
|
||||
|
||||
@@ -1339,6 +1339,21 @@
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "qb109597-e31e-46d7-7844-62e5fcf32ac8",
|
||||
"name": "email sub",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-usermodel-property-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"userinfo.token.claim": "true",
|
||||
"user.attribute": "email",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "sub",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "61c135e5-2447-494b-bc70-9612f383be27",
|
||||
"name": "email verified",
|
||||
|
||||
+7
-1
@@ -13,7 +13,13 @@
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
"matchManagers": ["npm"],
|
||||
"matchPackageNames": ["node", "node-fetch", "i18next-parser", "eslint"]
|
||||
"matchPackageNames": [
|
||||
"fetch-mock",
|
||||
"node",
|
||||
"node-fetch",
|
||||
"i18next-parser",
|
||||
"eslint"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
mkdir -p "$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/"
|
||||
PRE_COMMIT_FILE="$(dirname -- "${BASH_SOURCE[0]}")/../.git/hooks/pre-commit"
|
||||
|
||||
cat <<'EOF' >$PRE_COMMIT_FILE
|
||||
#!/bin/bash
|
||||
|
||||
# directories containing potential secrets
|
||||
DIRS="."
|
||||
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
|
||||
# allow to read user input, assigns stdin to keyboard
|
||||
exec </dev/tty
|
||||
|
||||
for d in $DIRS; do
|
||||
# find files containing secrets that should be encrypted
|
||||
for f in $(find "${d}" -type f -regex ".*\.enc\..*"); do
|
||||
if ! $(grep -q "unencrypted_suffix" $f); then
|
||||
printf '\xF0\x9F\x92\xA5 '
|
||||
echo "File $f has non encrypted secrets!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
EOF
|
||||
|
||||
chmod +x $PRE_COMMIT_FILE
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
git submodule update --init --recursive
|
||||
git submodule foreach 'git fetch origin; git checkout $(git rev-parse --abbrev-ref HEAD); git reset --hard origin/$(git rev-parse --abbrev-ref HEAD); git submodule update --recursive; git clean -dfx'
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name "*.enc.*" -exec sops updatekeys -y {} \;
|
||||
Submodule
+1
Submodule secrets added at 1485c6dc9d
@@ -91,7 +91,7 @@ class DocumentAdmin(admin.ModelAdmin):
|
||||
"""Document admin interface declaration."""
|
||||
|
||||
inlines = (DocumentAccessInline,)
|
||||
|
||||
|
||||
|
||||
@admin.register(models.Invitation)
|
||||
class InvitationAdmin(admin.ModelAdmin):
|
||||
@@ -119,4 +119,3 @@ class InvitationAdmin(admin.ModelAdmin):
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.issuer = request.user
|
||||
obj.save()
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
|
||||
from core import models
|
||||
|
||||
@@ -11,18 +10,10 @@ from core import models
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Serialize users."""
|
||||
|
||||
timezone = TimeZoneSerializerField(use_pytz=False, required=True)
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
"id",
|
||||
"language",
|
||||
"timezone",
|
||||
"is_device",
|
||||
"is_staff",
|
||||
]
|
||||
read_only_fields = ["id", "is_device", "is_staff"]
|
||||
fields = ["id", "email"]
|
||||
read_only_fields = ["id", "email"]
|
||||
|
||||
|
||||
class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
@@ -100,10 +91,19 @@ class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
class DocumentAccessSerializer(BaseAccessSerializer):
|
||||
"""Serialize document accesses."""
|
||||
|
||||
user_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=models.User.objects.all(),
|
||||
write_only=True,
|
||||
source="user",
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
user = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.DocumentAccess
|
||||
resource_field_name = "document"
|
||||
fields = ["id", "user", "team", "role", "abilities"]
|
||||
fields = ["id", "user", "user_id", "team", "role", "abilities"]
|
||||
read_only_fields = ["id", "abilities"]
|
||||
|
||||
|
||||
@@ -135,11 +135,21 @@ class DocumentSerializer(BaseResourceSerializer):
|
||||
"""Serialize documents."""
|
||||
|
||||
content = serializers.CharField(required=False)
|
||||
accesses = DocumentAccessSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Document
|
||||
fields = ["id", "content", "title", "accesses", "abilities", "is_public"]
|
||||
read_only_fields = ["id", "accesses", "abilities"]
|
||||
fields = [
|
||||
"id",
|
||||
"content",
|
||||
"title",
|
||||
"accesses",
|
||||
"abilities",
|
||||
"is_public",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "accesses", "abilities", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class TemplateSerializer(BaseResourceSerializer):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""API endpoints"""
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
@@ -112,8 +111,7 @@ class Pagination(pagination.PageNumberPagination):
|
||||
|
||||
|
||||
class UserViewSet(
|
||||
mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
mixins.UpdateModelMixin, viewsets.GenericViewSet, mixins.ListModelMixin
|
||||
):
|
||||
"""User ViewSet"""
|
||||
|
||||
@@ -121,6 +119,26 @@ class UserViewSet(
|
||||
queryset = models.User.objects.all()
|
||||
serializer_class = serializers.UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Limit listed users by querying the email field with a trigram similarity
|
||||
search if a query is provided.
|
||||
Limit listed users by excluding users already in the document if a document_id
|
||||
is provided.
|
||||
"""
|
||||
queryset = self.queryset
|
||||
|
||||
if self.action == "list":
|
||||
# Exclude all users already in the given document
|
||||
if document_id := self.request.GET.get("document_id", ""):
|
||||
queryset = queryset.exclude(documentaccess__document_id=document_id)
|
||||
|
||||
# Filter users by email similarity
|
||||
if query := self.request.GET.get("q", ""):
|
||||
queryset = queryset.filter(email__trigram_word_similar=query)
|
||||
|
||||
return queryset
|
||||
|
||||
@decorators.action(
|
||||
detail=False,
|
||||
methods=["get"],
|
||||
@@ -142,7 +160,7 @@ class ResourceViewsetMixin:
|
||||
"""Mixin with methods common to all resource viewsets that are managed with accesses."""
|
||||
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
ordering_fields = ["created_at"]
|
||||
ordering_fields = ["created_at", "updated_at", "title"]
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -293,6 +311,19 @@ class DocumentViewSet(
|
||||
resource_field_name = "document"
|
||||
queryset = models.Document.objects.all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Override perform_create to use the provided ID in the payload if it exists
|
||||
"""
|
||||
document_id = self.request.data.get("id")
|
||||
document = serializer.save(id=document_id) if document_id else serializer.save()
|
||||
|
||||
self.access_model_class.objects.create(
|
||||
user=self.request.user,
|
||||
role=models.RoleChoices.OWNER,
|
||||
**{self.resource_field_name: document},
|
||||
)
|
||||
|
||||
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
||||
def versions_list(self, request, *args, **kwargs):
|
||||
"""
|
||||
@@ -368,15 +399,15 @@ class DocumentAccessViewSet(
|
||||
|
||||
POST /api/v1.0/documents/<resource_id>/accesses/ with expected data:
|
||||
- user: str
|
||||
- role: str [owner|admin|member]
|
||||
- role: str [administrator|editor|reader]
|
||||
Return newly created document access
|
||||
|
||||
PUT /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/ with expected data:
|
||||
- role: str [owner|admin|member]
|
||||
- role: str [owner|admin|editor|reader]
|
||||
Return updated document access
|
||||
|
||||
PATCH /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/ with expected data:
|
||||
- role: str [owner|admin|member]
|
||||
- role: str [owner|admin|editor|reader]
|
||||
Return partially updated document access
|
||||
|
||||
DELETE /api/v1.0/documents/<resource_id>/accesses/<document_access_id>/
|
||||
@@ -458,15 +489,15 @@ class TemplateAccessViewSet(
|
||||
|
||||
POST /api/v1.0/templates/<template_id>/accesses/ with expected data:
|
||||
- user: str
|
||||
- role: str [owner|admin|member]
|
||||
- role: str [administrator|editor|reader]
|
||||
Return newly created template access
|
||||
|
||||
PUT /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
|
||||
- role: str [owner|admin|member]
|
||||
- role: str [owner|admin|editor|reader]
|
||||
Return updated template access
|
||||
|
||||
PATCH /api/v1.0/templates/<template_id>/accesses/<template_access_id>/ with expected data:
|
||||
- role: str [owner|admin|member]
|
||||
- role: str [owner|admin|editor|reader]
|
||||
Return partially updated template access
|
||||
|
||||
DELETE /api/v1.0/templates/<template_id>/accesses/<template_access_id>/
|
||||
@@ -496,7 +527,7 @@ class InvitationViewset(
|
||||
|
||||
POST /api/v1.0/documents/<document_id>/invitations/ with expected data:
|
||||
- email: str
|
||||
- role: str [owner|admin|member]
|
||||
- role: str [administrator|editor|reader]
|
||||
Return newly created invitation (issuer and document are automatically set)
|
||||
|
||||
PUT / PATCH : Not permitted. Instead of updating your invitation,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.0.3 on 2024-04-19 11:38
|
||||
# Generated by Django 5.0.3 on 2024-05-28 20:29
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.core.validators
|
||||
@@ -89,7 +89,7 @@ class Migration(migrations.Migration):
|
||||
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||
('team', models.CharField(blank=True, max_length=100)),
|
||||
('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
|
||||
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.document')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
@@ -100,6 +100,23 @@ class Migration(migrations.Migration):
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Invitation',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='email address')),
|
||||
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
|
||||
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.document')),
|
||||
('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document invitation',
|
||||
'verbose_name_plural': 'Document invitations',
|
||||
'db_table': 'impress_invitation',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TemplateAccess',
|
||||
fields=[
|
||||
@@ -107,7 +124,7 @@ class Migration(migrations.Migration):
|
||||
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||
('team', models.CharField(blank=True, max_length=100)),
|
||||
('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||
('role', models.CharField(choices=[('reader', 'Reader'), ('editor', 'Editor'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='reader', max_length=20)),
|
||||
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='core.template')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
@@ -130,6 +147,10 @@ class Migration(migrations.Migration):
|
||||
model_name='documentaccess',
|
||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='invitation',
|
||||
constraint=models.UniqueConstraint(fields=('email', 'document'), name='email_and_document_unique_together'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='templateaccess',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('user__isnull', False)), fields=('user', 'template'), name='unique_template_user', violation_error_message='This user is already in this template.'),
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Generated by Django 5.0.3 on 2024-05-12 19:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='language',
|
||||
field=models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Invitation',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='email address')),
|
||||
('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)),
|
||||
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.document')),
|
||||
('issuer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document invitation',
|
||||
'verbose_name_plural': 'Document invitations',
|
||||
'db_table': 'impress_invitation',
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='invitation',
|
||||
constraint=models.UniqueConstraint(fields=('email', 'document'), name='email_and_document_unique_together'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
"CREATE EXTENSION IF NOT EXISTS pg_trgm;",
|
||||
reverse_sql="DROP EXTENSION IF EXISTS pg_trgm;",
|
||||
),
|
||||
]
|
||||
+32
-16
@@ -2,7 +2,6 @@
|
||||
Declare and configure the models for the impress core application
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import smtplib
|
||||
import textwrap
|
||||
import uuid
|
||||
@@ -55,8 +54,9 @@ def get_resource_roles(resource, user):
|
||||
class RoleChoices(models.TextChoices):
|
||||
"""Defines the possible roles a user can have in a template."""
|
||||
|
||||
MEMBER = "member", _("Member")
|
||||
ADMIN = "administrator", _("Administrator")
|
||||
READER = "reader", _("Reader") # Can read
|
||||
EDITOR = "editor", _("Editor") # Can read and edit
|
||||
ADMIN = "administrator", _("Administrator") # Can read, edit, delete and share
|
||||
OWNER = "owner", _("Owner")
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ class BaseAccess(BaseModel):
|
||||
)
|
||||
team = models.CharField(max_length=100, blank=True)
|
||||
role = models.CharField(
|
||||
max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER
|
||||
max_length=20, choices=RoleChoices.choices, default=RoleChoices.READER
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -265,14 +265,20 @@ class BaseAccess(BaseModel):
|
||||
RoleChoices.OWNER in roles
|
||||
and resource.accesses.filter(role=RoleChoices.OWNER).count() > 1
|
||||
)
|
||||
set_role_to = [RoleChoices.ADMIN, RoleChoices.MEMBER] if can_delete else []
|
||||
set_role_to = (
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
if can_delete
|
||||
else []
|
||||
)
|
||||
else:
|
||||
can_delete = is_owner_or_admin
|
||||
set_role_to = []
|
||||
if RoleChoices.OWNER in roles:
|
||||
set_role_to.append(RoleChoices.OWNER)
|
||||
if is_owner_or_admin:
|
||||
set_role_to.extend([RoleChoices.ADMIN, RoleChoices.MEMBER])
|
||||
set_role_to.extend(
|
||||
[RoleChoices.ADMIN, RoleChoices.EDITOR, RoleChoices.READER]
|
||||
)
|
||||
|
||||
# Remove the current role as we don't want to propose it as an option
|
||||
try:
|
||||
@@ -283,6 +289,7 @@ class BaseAccess(BaseModel):
|
||||
return {
|
||||
"destroy": can_delete,
|
||||
"update": bool(set_role_to),
|
||||
"partial_update": bool(set_role_to),
|
||||
"retrieve": bool(roles),
|
||||
"set_role_to": set_role_to,
|
||||
}
|
||||
@@ -309,12 +316,19 @@ class Document(BaseModel):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def key_base(self):
|
||||
"""Key base of the location where the document is stored in object storage."""
|
||||
if not self.pk:
|
||||
raise RuntimeError(
|
||||
"The document instance must be saved before requesting a storage key."
|
||||
)
|
||||
return str(self.pk)
|
||||
|
||||
@property
|
||||
def file_key(self):
|
||||
"""Key of the object storage file to which the document content is stored"""
|
||||
if not self.pk:
|
||||
return None
|
||||
return str(self.pk)
|
||||
return f"{self.key_base}/file"
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
@@ -325,7 +339,7 @@ class Document(BaseModel):
|
||||
except (FileNotFoundError, ClientError):
|
||||
pass
|
||||
else:
|
||||
self._content = response["Body"].read().decode('utf-8')
|
||||
self._content = response["Body"].read().decode("utf-8")
|
||||
return self._content
|
||||
|
||||
@content.setter
|
||||
@@ -333,7 +347,7 @@ class Document(BaseModel):
|
||||
"""Cache the content, don't write to object storage yet"""
|
||||
if not isinstance(content, str):
|
||||
raise ValueError("content should be a string.")
|
||||
|
||||
|
||||
self._content = content
|
||||
|
||||
def get_content_response(self, version_id=""):
|
||||
@@ -447,6 +461,7 @@ class Document(BaseModel):
|
||||
is_owner_or_admin = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
is_editor = bool(RoleChoices.EDITOR in roles)
|
||||
can_get = self.is_public or bool(roles)
|
||||
can_get_versions = bool(roles)
|
||||
|
||||
@@ -456,8 +471,8 @@ class Document(BaseModel):
|
||||
"versions_list": can_get_versions,
|
||||
"versions_retrieve": can_get_versions,
|
||||
"manage_accesses": is_owner_or_admin,
|
||||
"update": is_owner_or_admin,
|
||||
"partial_update": is_owner_or_admin,
|
||||
"update": is_owner_or_admin or is_editor,
|
||||
"partial_update": is_owner_or_admin or is_editor,
|
||||
"retrieve": can_get,
|
||||
}
|
||||
|
||||
@@ -537,14 +552,15 @@ class Template(BaseModel):
|
||||
is_owner_or_admin = bool(
|
||||
set(roles).intersection({RoleChoices.OWNER, RoleChoices.ADMIN})
|
||||
)
|
||||
is_editor = bool(RoleChoices.EDITOR in roles)
|
||||
can_get = self.is_public or bool(roles)
|
||||
|
||||
return {
|
||||
"destroy": RoleChoices.OWNER in roles,
|
||||
"generate_document": can_get,
|
||||
"manage_accesses": is_owner_or_admin,
|
||||
"update": is_owner_or_admin,
|
||||
"partial_update": is_owner_or_admin,
|
||||
"update": is_owner_or_admin or is_editor,
|
||||
"partial_update": is_owner_or_admin or is_editor,
|
||||
"retrieve": can_get,
|
||||
}
|
||||
|
||||
@@ -631,7 +647,7 @@ class Invitation(BaseModel):
|
||||
related_name="invitations",
|
||||
)
|
||||
role = models.CharField(
|
||||
max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER
|
||||
max_length=20, choices=RoleChoices.choices, default=RoleChoices.READER
|
||||
)
|
||||
issuer = models.ForeignKey(
|
||||
User,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Tests for Documents API endpoint in impress's core app: create
|
||||
"""
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -45,3 +47,26 @@ def test_api_documents_create_authenticated():
|
||||
document = Document.objects.get()
|
||||
assert document.title == "my document"
|
||||
assert document.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
|
||||
def test_api_documents_create_with_id_from_payload():
|
||||
"""
|
||||
We should be able to create a document with an ID from the payload.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
doc_id = uuid.uuid4()
|
||||
response = client.post(
|
||||
"/api/v1.0/documents/",
|
||||
{"title": "my document", "id": str(doc_id)},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
document = Document.objects.get()
|
||||
assert document.title == "my document"
|
||||
assert document.id == doc_id
|
||||
assert document.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
@@ -45,14 +45,12 @@ def test_api_documents_delete_authenticated_unrelated():
|
||||
assert models.Document.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["member", "administrator"])
|
||||
@pytest.mark.parametrize("role", ["reader", "editor", "administrator"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_delete_authenticated_member_or_administrator(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
def test_api_documents_delete_authenticated_not_owner(via, role, mock_user_get_teams):
|
||||
"""
|
||||
Authenticated users should not be allowed to delete a document for which they are
|
||||
only a member or administrator.
|
||||
only a reader, editor or administrator.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ Tests for Documents API endpoint in impress's core app: list
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from faker import Faker
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
|
||||
fake = Faker()
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@@ -166,7 +168,7 @@ def test_api_documents_list_authenticated_distinct():
|
||||
assert content["results"][0]["id"] == str(document.id)
|
||||
|
||||
|
||||
def test_api_documents_order():
|
||||
def test_api_documents_order_created_at_desc():
|
||||
"""
|
||||
Test that the endpoint GET documents is sorted in 'created_at' descending order by default.
|
||||
"""
|
||||
@@ -174,11 +176,13 @@ def test_api_documents_order():
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document_ids = [
|
||||
str(document.id)
|
||||
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/",
|
||||
)
|
||||
@@ -186,15 +190,16 @@ def test_api_documents_order():
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
response_document_ids = [document["id"] for document in response_data["results"]]
|
||||
response_document_created = [
|
||||
document["created_at"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
document_ids.reverse()
|
||||
assert (
|
||||
response_document_ids == document_ids
|
||||
response_document_created == documents_created
|
||||
), "created_at values are not sorted from newest to oldest"
|
||||
|
||||
|
||||
def test_api_documents_order_param():
|
||||
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.
|
||||
@@ -203,20 +208,152 @@ def test_api_documents_order_param():
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
documents_ids = [
|
||||
str(document.id)
|
||||
documents_created = [
|
||||
document.created_at.isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
|
||||
response = APIClient().get(
|
||||
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)
|
||||
|
||||
documents_updated = [
|
||||
document.updated_at.isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
|
||||
documents_updated.sort(reverse=True)
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?ordering=-updated_at",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
response_document_ids = [document["id"] for document in response_data["results"]]
|
||||
response_document_updated = [
|
||||
document["updated_at"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
assert (
|
||||
response_document_ids == documents_ids
|
||||
), "created_at values are not sorted from oldest to newest"
|
||||
response_document_updated == documents_updated
|
||||
), "updated_at values are not sorted from newest to oldest"
|
||||
|
||||
|
||||
def test_api_documents_order_updated_at_asc():
|
||||
"""
|
||||
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)
|
||||
|
||||
documents_updated = [
|
||||
document.updated_at.isoformat().replace("+00:00", "Z")
|
||||
for document in factories.DocumentFactory.create_batch(5, is_public=True)
|
||||
]
|
||||
|
||||
documents_updated.sort()
|
||||
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/documents/?ordering=updated_at",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
response_document_updated = [
|
||||
document["updated_at"] for document in response_data["results"]
|
||||
]
|
||||
|
||||
assert (
|
||||
response_document_updated == documents_updated
|
||||
), "updated_at values are not sorted from oldest to newest"
|
||||
|
||||
|
||||
def test_api_documents_order_title_desc():
|
||||
"""
|
||||
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)
|
||||
|
||||
documents_title = [
|
||||
factories.DocumentFactory(is_public=True, title=fake.sentence(nb_words=4)).title
|
||||
for _ in range(5)
|
||||
]
|
||||
|
||||
documents_title.sort(reverse=True)
|
||||
|
||||
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 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"
|
||||
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.api import serializers
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -32,6 +33,8 @@ def test_api_documents_retrieve_anonymous_public():
|
||||
"title": document.title,
|
||||
"is_public": True,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +80,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public():
|
||||
"title": document.title,
|
||||
"is_public": True,
|
||||
"content": document.content,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
|
||||
@@ -112,30 +117,32 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
document = factories.DocumentFactory()
|
||||
access1 = factories.UserDocumentAccessFactory(document=document, user=user)
|
||||
access2 = factories.UserDocumentAccessFactory(document=document)
|
||||
access1_user = serializers.UserSerializer(instance=user).data
|
||||
access2_user = serializers.UserSerializer(instance=access2.user).data
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["user"]) == sorted(
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access1.id),
|
||||
"user": str(user.id),
|
||||
"user": access1_user,
|
||||
"team": "",
|
||||
"role": access1.role,
|
||||
"abilities": access1.get_abilities(user),
|
||||
},
|
||||
{
|
||||
"id": str(access2.id),
|
||||
"user": str(access2.user.id),
|
||||
"user": access2_user,
|
||||
"team": "",
|
||||
"role": access2.role,
|
||||
"abilities": access2.get_abilities(user),
|
||||
},
|
||||
],
|
||||
key=lambda x: x["user"],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
assert response.json() == {
|
||||
"id": str(document.id),
|
||||
@@ -143,6 +150,8 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_public": document.is_public,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +170,10 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_te
|
||||
document = factories.DocumentFactory(is_public=False)
|
||||
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="members", role="member"
|
||||
document=document, team="readers", role="reader"
|
||||
)
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="editors", role="editor"
|
||||
)
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="administrators", role="administrator"
|
||||
@@ -178,8 +190,10 @@ def test_api_documents_retrieve_authenticated_related_team_none(mock_user_get_te
|
||||
@pytest.mark.parametrize(
|
||||
"teams",
|
||||
[
|
||||
["members"],
|
||||
["unknown", "members"],
|
||||
["readers"],
|
||||
["unknown", "readers"],
|
||||
["editors"],
|
||||
["unknown", "editors"],
|
||||
],
|
||||
)
|
||||
def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
@@ -198,8 +212,11 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
|
||||
document = factories.DocumentFactory(is_public=False)
|
||||
|
||||
access_member = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="members", role="member"
|
||||
access_reader = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="readers", role="reader"
|
||||
)
|
||||
access_editor = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="editors", role="editor"
|
||||
)
|
||||
access_administrator = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="administrators", role="administrator"
|
||||
@@ -218,14 +235,22 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"retrieve": True,
|
||||
"set_role_to": [],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
}
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access_member.id),
|
||||
"id": str(access_reader.id),
|
||||
"user": None,
|
||||
"team": "members",
|
||||
"role": access_member.role,
|
||||
"team": "readers",
|
||||
"role": access_reader.role,
|
||||
"abilities": expected_abilities,
|
||||
},
|
||||
{
|
||||
"id": str(access_editor.id),
|
||||
"user": None,
|
||||
"team": "editors",
|
||||
"role": access_editor.role,
|
||||
"abilities": expected_abilities,
|
||||
},
|
||||
{
|
||||
@@ -258,6 +283,8 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_public": False,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +292,7 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"teams",
|
||||
[
|
||||
["administrators"],
|
||||
["members", "administrators"],
|
||||
["editors", "administrators"],
|
||||
["unknown", "administrators"],
|
||||
],
|
||||
)
|
||||
@@ -285,8 +312,11 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
|
||||
document = factories.DocumentFactory(is_public=False)
|
||||
|
||||
access_member = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="members", role="member"
|
||||
access_reader = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="readers", role="reader"
|
||||
)
|
||||
access_editor = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="editors", role="editor"
|
||||
)
|
||||
access_administrator = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="administrators", role="administrator"
|
||||
@@ -305,15 +335,29 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access_member.id),
|
||||
"id": str(access_reader.id),
|
||||
"user": None,
|
||||
"team": "members",
|
||||
"role": "member",
|
||||
"team": "readers",
|
||||
"role": "reader",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["administrator"],
|
||||
"set_role_to": ["administrator", "editor"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": str(access_editor.id),
|
||||
"user": None,
|
||||
"team": "editors",
|
||||
"role": "editor",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["administrator", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -324,8 +368,9 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["member"],
|
||||
"set_role_to": ["editor", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -338,6 +383,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"retrieve": True,
|
||||
"set_role_to": [],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -356,6 +402,8 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_public": False,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
|
||||
@@ -384,8 +432,11 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
|
||||
document = factories.DocumentFactory(is_public=False)
|
||||
|
||||
access_member = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="members", role="member"
|
||||
access_reader = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="readers", role="reader"
|
||||
)
|
||||
access_editor = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="editors", role="editor"
|
||||
)
|
||||
access_administrator = factories.TeamDocumentAccessFactory(
|
||||
document=document, team="administrators", role="administrator"
|
||||
@@ -404,15 +455,29 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access_member.id),
|
||||
"id": str(access_reader.id),
|
||||
"user": None,
|
||||
"team": "members",
|
||||
"role": "member",
|
||||
"team": "readers",
|
||||
"role": "reader",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["owner", "administrator"],
|
||||
"set_role_to": ["owner", "administrator", "editor"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": str(access_editor.id),
|
||||
"user": None,
|
||||
"team": "editors",
|
||||
"role": "editor",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["owner", "administrator", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -423,8 +488,9 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["owner", "member"],
|
||||
"set_role_to": ["owner", "editor", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -436,10 +502,11 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
# editable only if there is another owner role than the user's team...
|
||||
"destroy": other_access.role == "owner",
|
||||
"retrieve": True,
|
||||
"set_role_to": ["administrator", "member"]
|
||||
"set_role_to": ["administrator", "editor", "reader"]
|
||||
if other_access.role == "owner"
|
||||
else [],
|
||||
"update": other_access.role == "owner",
|
||||
"partial_update": other_access.role == "owner",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -458,4 +525,6 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_public": False,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
|
||||
@@ -66,9 +66,9 @@ def test_api_documents_update_authenticated_unrelated():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_update_authenticated_members(via, mock_user_get_teams):
|
||||
def test_api_documents_update_authenticated_reader(via, mock_user_get_teams):
|
||||
"""
|
||||
Users who are members of a document but not administrators should
|
||||
Users who are editors or reader of a document but not administrators should
|
||||
not be allowed to update it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
@@ -78,11 +78,11 @@ def test_api_documents_update_authenticated_members(via, mock_user_get_teams):
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="member"
|
||||
document=document, team="lasuite", role="reader"
|
||||
)
|
||||
|
||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||
@@ -106,12 +106,12 @@ def test_api_documents_update_authenticated_members(via, mock_user_get_teams):
|
||||
assert document_values == old_document_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["administrator", "owner"])
|
||||
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_update_authenticated_administrator_or_owner(
|
||||
def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Administrator or owner of a document should be allowed to update it."""
|
||||
"""A user who is editor, administrator or owner of a document should be allowed to update it."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -141,8 +141,10 @@ def test_api_documents_update_authenticated_administrator_or_owner(
|
||||
document = models.Document.objects.get(pk=document.pk)
|
||||
document_values = serializers.DocumentSerializer(instance=document).data
|
||||
for key, value in document_values.items():
|
||||
if key in ["id", "accesses"]:
|
||||
if key in ["id", "accesses", "created_at"]:
|
||||
assert value == old_document_values[key]
|
||||
elif key == "updated_at":
|
||||
assert value > old_document_values[key]
|
||||
else:
|
||||
assert value == new_document_values[key]
|
||||
|
||||
@@ -178,8 +180,10 @@ def test_api_documents_update_authenticated_owners(via, mock_user_get_teams):
|
||||
document = models.Document.objects.get(pk=document.pk)
|
||||
document_values = serializers.DocumentSerializer(instance=document).data
|
||||
for key, value in document_values.items():
|
||||
if key in ["id", "accesses"]:
|
||||
if key in ["id", "accesses", "created_at"]:
|
||||
assert value == old_document_values[key]
|
||||
elif key == "updated_at":
|
||||
assert value > old_document_values[key]
|
||||
else:
|
||||
assert value == new_document_values[key]
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ def test_api_templates_delete_authenticated_unrelated():
|
||||
assert models.Template.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["member", "administrator"])
|
||||
@pytest.mark.parametrize("role", ["reader", "editor", "administrator"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_templates_delete_authenticated_member_or_administrator(
|
||||
via, role, mock_user_get_teams
|
||||
|
||||
@@ -160,7 +160,10 @@ def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_te
|
||||
template = factories.TemplateFactory(is_public=False)
|
||||
|
||||
factories.TeamTemplateAccessFactory(
|
||||
template=template, team="members", role="member"
|
||||
template=template, team="readers", role="reader"
|
||||
)
|
||||
factories.TeamTemplateAccessFactory(
|
||||
template=template, team="editors", role="editor"
|
||||
)
|
||||
factories.TeamTemplateAccessFactory(
|
||||
template=template, team="administrators", role="administrator"
|
||||
@@ -177,11 +180,13 @@ def test_api_templates_retrieve_authenticated_related_team_none(mock_user_get_te
|
||||
@pytest.mark.parametrize(
|
||||
"teams",
|
||||
[
|
||||
["members"],
|
||||
["unknown", "members"],
|
||||
["readers"],
|
||||
["unknown", "readers"],
|
||||
["editors"],
|
||||
["unknown", "editors"],
|
||||
],
|
||||
)
|
||||
def test_api_templates_retrieve_authenticated_related_team_members(
|
||||
def test_api_templates_retrieve_authenticated_related_team_readers_or_editors(
|
||||
teams, mock_user_get_teams
|
||||
):
|
||||
"""
|
||||
@@ -197,8 +202,11 @@ def test_api_templates_retrieve_authenticated_related_team_members(
|
||||
|
||||
template = factories.TemplateFactory(is_public=False)
|
||||
|
||||
access_member = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="members", role="member"
|
||||
access_reader = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="readers", role="reader"
|
||||
)
|
||||
access_editor = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="editors", role="editor"
|
||||
)
|
||||
access_administrator = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="administrators", role="administrator"
|
||||
@@ -217,14 +225,22 @@ def test_api_templates_retrieve_authenticated_related_team_members(
|
||||
"retrieve": True,
|
||||
"set_role_to": [],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
}
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access_member.id),
|
||||
"id": str(access_reader.id),
|
||||
"user": None,
|
||||
"team": "members",
|
||||
"role": access_member.role,
|
||||
"team": "readers",
|
||||
"role": access_reader.role,
|
||||
"abilities": expected_abilities,
|
||||
},
|
||||
{
|
||||
"id": str(access_editor.id),
|
||||
"user": None,
|
||||
"team": "editors",
|
||||
"role": access_editor.role,
|
||||
"abilities": expected_abilities,
|
||||
},
|
||||
{
|
||||
@@ -285,8 +301,11 @@ def test_api_templates_retrieve_authenticated_related_team_administrators(
|
||||
|
||||
template = factories.TemplateFactory(is_public=False)
|
||||
|
||||
access_member = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="members", role="member"
|
||||
access_reader = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="readers", role="reader"
|
||||
)
|
||||
access_editor = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="editors", role="editor"
|
||||
)
|
||||
access_administrator = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="administrators", role="administrator"
|
||||
@@ -304,15 +323,29 @@ def test_api_templates_retrieve_authenticated_related_team_administrators(
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access_member.id),
|
||||
"id": str(access_reader.id),
|
||||
"user": None,
|
||||
"team": "members",
|
||||
"role": "member",
|
||||
"team": "readers",
|
||||
"role": "reader",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["administrator"],
|
||||
"set_role_to": ["administrator", "editor"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": str(access_editor.id),
|
||||
"user": None,
|
||||
"team": "editors",
|
||||
"role": "editor",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["administrator", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -323,8 +356,9 @@ def test_api_templates_retrieve_authenticated_related_team_administrators(
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["member"],
|
||||
"set_role_to": ["editor", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -337,6 +371,7 @@ def test_api_templates_retrieve_authenticated_related_team_administrators(
|
||||
"retrieve": True,
|
||||
"set_role_to": [],
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -384,8 +419,11 @@ def test_api_templates_retrieve_authenticated_related_team_owners(
|
||||
|
||||
template = factories.TemplateFactory(is_public=False)
|
||||
|
||||
access_member = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="members", role="member"
|
||||
access_reader = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="readers", role="reader"
|
||||
)
|
||||
access_editor = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="editors", role="editor"
|
||||
)
|
||||
access_administrator = factories.TeamTemplateAccessFactory(
|
||||
template=template, team="administrators", role="administrator"
|
||||
@@ -403,15 +441,29 @@ def test_api_templates_retrieve_authenticated_related_team_owners(
|
||||
assert sorted(content.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(access_member.id),
|
||||
"id": str(access_reader.id),
|
||||
"user": None,
|
||||
"team": "members",
|
||||
"role": "member",
|
||||
"team": "readers",
|
||||
"role": "reader",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["owner", "administrator"],
|
||||
"set_role_to": ["owner", "administrator", "editor"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": str(access_editor.id),
|
||||
"user": None,
|
||||
"team": "editors",
|
||||
"role": "editor",
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["owner", "administrator", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -422,8 +474,9 @@ def test_api_templates_retrieve_authenticated_related_team_owners(
|
||||
"abilities": {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"set_role_to": ["owner", "member"],
|
||||
"set_role_to": ["owner", "editor", "reader"],
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -435,10 +488,11 @@ def test_api_templates_retrieve_authenticated_related_team_owners(
|
||||
# editable only if there is another owner role than the user's team...
|
||||
"destroy": other_access.role == "owner",
|
||||
"retrieve": True,
|
||||
"set_role_to": ["administrator", "member"]
|
||||
"set_role_to": ["administrator", "editor", "reader"]
|
||||
if other_access.role == "owner"
|
||||
else [],
|
||||
"update": other_access.role == "owner",
|
||||
"partial_update": other_access.role == "owner",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -66,10 +66,9 @@ def test_api_templates_update_authenticated_unrelated():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_templates_update_authenticated_members(via, mock_user_get_teams):
|
||||
def test_api_templates_update_authenticated_readers(via, mock_user_get_teams):
|
||||
"""
|
||||
Users who are members of a template but not administrators should
|
||||
not be allowed to update it.
|
||||
Users who are readers of a template should not be allowed to update it.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -78,11 +77,11 @@ def test_api_templates_update_authenticated_members(via, mock_user_get_teams):
|
||||
|
||||
template = factories.TemplateFactory()
|
||||
if via == USER:
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role="member")
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role="reader")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamTemplateAccessFactory(
|
||||
template=template, team="lasuite", role="member"
|
||||
template=template, team="lasuite", role="reader"
|
||||
)
|
||||
|
||||
old_template_values = serializers.TemplateSerializer(instance=template).data
|
||||
@@ -106,9 +105,9 @@ def test_api_templates_update_authenticated_members(via, mock_user_get_teams):
|
||||
assert template_values == old_template_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["administrator", "owner"])
|
||||
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_templates_update_authenticated_administrator_or_owner(
|
||||
def test_api_templates_update_authenticated_editor_or_administrator_or_owner(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Administrator or owner of a template should be allowed to update it."""
|
||||
|
||||
@@ -92,6 +92,9 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_get_tea
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
)
|
||||
|
||||
access2_user = serializers.UserSerializer(instance=access2.user).data
|
||||
base_user = serializers.UserSerializer(instance=user).data
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 3
|
||||
@@ -99,7 +102,7 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_get_tea
|
||||
[
|
||||
{
|
||||
"id": str(user_access.id),
|
||||
"user": str(user.id) if via == "user" else None,
|
||||
"user": base_user if via == "user" else None,
|
||||
"team": "lasuite" if via == "team" else "",
|
||||
"role": user_access.role,
|
||||
"abilities": user_access.get_abilities(user),
|
||||
@@ -113,7 +116,7 @@ def test_api_document_accesses_list_authenticated_related(via, mock_user_get_tea
|
||||
},
|
||||
{
|
||||
"id": str(access2.id),
|
||||
"user": str(access2.user.id),
|
||||
"user": access2_user,
|
||||
"team": "",
|
||||
"role": access2.role,
|
||||
"abilities": access2.get_abilities(user),
|
||||
@@ -197,10 +200,12 @@ def test_api_document_accesses_retrieve_authenticated_related(via, mock_user_get
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
access_user = serializers.UserSerializer(instance=access.user).data
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(access.id),
|
||||
"user": str(access.user.id),
|
||||
"user": access_user,
|
||||
"team": "",
|
||||
"role": access.role,
|
||||
"abilities": access.get_abilities(user),
|
||||
@@ -254,9 +259,12 @@ def test_api_document_accesses_create_authenticated_unrelated():
|
||||
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_member(via, mock_user_get_teams):
|
||||
"""Members of a document should not be allowed to create document accesses."""
|
||||
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()
|
||||
@@ -264,21 +272,21 @@ def test_api_document_accesses_create_authenticated_member(via, mock_user_get_te
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
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="member"
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
for role in [role[0] for role in models.RoleChoices.choices]:
|
||||
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": role,
|
||||
"role": new_role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -337,7 +345,7 @@ def test_api_document_accesses_create_authenticated_administrator(
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"user_id": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
@@ -346,12 +354,13 @@ def test_api_document_accesses_create_authenticated_administrator(
|
||||
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": str(other_user.id),
|
||||
"user": other_user,
|
||||
}
|
||||
|
||||
|
||||
@@ -381,7 +390,7 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_get_tea
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"user_id": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
@@ -390,9 +399,10 @@ def test_api_document_accesses_create_authenticated_owner(via, mock_user_get_tea
|
||||
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": str(other_user.id),
|
||||
"user": other_user,
|
||||
"team": "",
|
||||
"role": role,
|
||||
"abilities": new_document_access.get_abilities(user),
|
||||
@@ -456,9 +466,12 @@ def test_api_document_accesses_update_authenticated_unrelated():
|
||||
assert updated_values == old_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_update_authenticated_member(via, mock_user_get_teams):
|
||||
"""Members of a document should not be allowed to update its accesses."""
|
||||
def test_api_document_accesses_update_authenticated_reader_or_editor(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Readers or editors of a document should not be allowed to update its accesses."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -466,11 +479,11 @@ def test_api_document_accesses_update_authenticated_member(via, mock_user_get_te
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
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="member"
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
access = factories.UserDocumentAccessFactory(document=document)
|
||||
@@ -521,14 +534,14 @@ def test_api_document_accesses_update_administrator_except_owner(
|
||||
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
document=document,
|
||||
role=random.choice(["administrator", "member"]),
|
||||
role=random.choice(["administrator", "editor", "reader"]),
|
||||
)
|
||||
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
|
||||
new_values = {
|
||||
"id": uuid4(),
|
||||
"user_id": factories.UserFactory().id,
|
||||
"role": random.choice(["administrator", "member"]),
|
||||
"role": random.choice(["administrator", "editor", "reader"]),
|
||||
}
|
||||
|
||||
for field, value in new_values.items():
|
||||
@@ -629,7 +642,7 @@ def test_api_document_accesses_update_administrator_to_owner(via, mock_user_get_
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
document=document,
|
||||
user=other_user,
|
||||
role=random.choice(["administrator", "member"]),
|
||||
role=random.choice(["administrator", "editor", "reader"]),
|
||||
)
|
||||
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
|
||||
@@ -736,7 +749,7 @@ def test_api_document_accesses_update_owner_self(via, mock_user_get_teams):
|
||||
)
|
||||
|
||||
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
new_role = random.choice(["administrator", "member"])
|
||||
new_role = random.choice(["administrator", "editor", "reader"])
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
@@ -753,7 +766,13 @@ def test_api_document_accesses_update_owner_self(via, mock_user_get_teams):
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data={**old_values, "role": new_role},
|
||||
data={
|
||||
**old_values,
|
||||
"role": new_role,
|
||||
"user_id": old_values.get("user", {}).get("id")
|
||||
if old_values.get("user") is not None
|
||||
else None,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
@@ -797,11 +816,12 @@ def test_api_document_accesses_delete_authenticated():
|
||||
assert models.DocumentAccess.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_delete_member(via, mock_user_get_teams):
|
||||
def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_get_teams):
|
||||
"""
|
||||
Authenticated users should not be allowed to delete a document access for a
|
||||
document in which they are a simple member.
|
||||
document in which they are a simple reader or editor.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -810,11 +830,11 @@ def test_api_document_accesses_delete_member(via, mock_user_get_teams):
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
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="member"
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
access = factories.UserDocumentAccessFactory(document=document)
|
||||
@@ -855,7 +875,7 @@ def test_api_document_accesses_delete_administrators_except_owners(
|
||||
)
|
||||
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
document=document, role=random.choice(["member", "administrator"])
|
||||
document=document, role=random.choice(["reader", "editor", "administrator"])
|
||||
)
|
||||
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
|
||||
@@ -57,13 +57,20 @@ def test_api_document_invitations__create__authenticated_outsider():
|
||||
@pytest.mark.parametrize(
|
||||
"inviting,invited,is_allowed",
|
||||
(
|
||||
["member", "member", False],
|
||||
["member", "administrator", False],
|
||||
["member", "owner", False],
|
||||
["administrator", "member", True],
|
||||
["reader", "reader", False],
|
||||
["reader", "editor", False],
|
||||
["reader", "administrator", False],
|
||||
["reader", "owner", False],
|
||||
["editor", "reader", False],
|
||||
["editor", "editor", False],
|
||||
["editor", "administrator", False],
|
||||
["editor", "owner", False],
|
||||
["administrator", "reader", True],
|
||||
["administrator", "editor", True],
|
||||
["administrator", "administrator", True],
|
||||
["administrator", "owner", False],
|
||||
["owner", "member", True],
|
||||
["owner", "reader", True],
|
||||
["owner", "editor", True],
|
||||
["owner", "administrator", True],
|
||||
["owner", "owner", True],
|
||||
),
|
||||
@@ -146,7 +153,7 @@ def test_api_document_invitations__create__cannot_duplicate_invitation():
|
||||
# Create a new invitation to the same document with the exact same email address
|
||||
invitation_values = {
|
||||
"email": existing_invitation.email,
|
||||
"role": random.choice(["administrator", "member"]),
|
||||
"role": random.choice(["administrator", "editor", "reader"]),
|
||||
}
|
||||
|
||||
client = APIClient()
|
||||
@@ -222,12 +229,12 @@ def test_api_document_invitations__list__authenticated(
|
||||
document=document, role="administrator", issuer=user
|
||||
)
|
||||
other_invitations = factories.InvitationFactory.create_batch(
|
||||
2, document=document, role="member", issuer=other_user
|
||||
2, document=document, role="reader", issuer=other_user
|
||||
)
|
||||
|
||||
# invitations from other documents should not be listed
|
||||
other_document = factories.DocumentFactory()
|
||||
factories.InvitationFactory.create_batch(2, document=other_document, role="member")
|
||||
factories.InvitationFactory.create_batch(2, document=other_document, role="reader")
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
@@ -275,7 +282,7 @@ def test_api_document_invitations__list__expired_invitations_still_listed(settin
|
||||
settings.INVITATION_VALIDITY_DURATION = 1 # second
|
||||
expired_invitation = factories.InvitationFactory(
|
||||
document=document,
|
||||
role="member",
|
||||
role="reader",
|
||||
issuer=user,
|
||||
)
|
||||
time.sleep(1)
|
||||
@@ -467,17 +474,20 @@ def test_api_document_invitations__delete__privileged_members(
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_invitations__delete__members(via, mock_user_get_teams):
|
||||
"""Member should not be able to cancel invitation."""
|
||||
def test_api_document_invitations_delete_readers_or_editors(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Readers or editors should not be able to cancel invitation."""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
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="member"
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
|
||||
@@ -437,11 +437,12 @@ def test_api_document_versions_delete_authenticated_private():
|
||||
assert response.json() == {"detail": "Not found."}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_versions_delete_member(via, mock_user_get_teams):
|
||||
def test_api_document_versions_delete_reader_or_editor(via, role, mock_user_get_teams):
|
||||
"""
|
||||
Authenticated users should not be allowed to delete a document version for a
|
||||
document in which they are a simple member.
|
||||
document in which they are a simple reader or editor.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -450,11 +451,11 @@ def test_api_document_versions_delete_member(via, mock_user_get_teams):
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
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="member"
|
||||
document=document, team="lasuite", role=role
|
||||
)
|
||||
|
||||
# Create a new version should make it available to the user
|
||||
|
||||
@@ -254,9 +254,12 @@ def test_api_template_accesses_create_authenticated_unrelated():
|
||||
assert not models.TemplateAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_template_accesses_create_authenticated_member(via, mock_user_get_teams):
|
||||
"""Members of a template should not be allowed to create template accesses."""
|
||||
def test_api_template_accesses_create_authenticated_editor_or_reader(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Editors or readers of a template should not be allowed to create template accesses."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -264,21 +267,21 @@ def test_api_template_accesses_create_authenticated_member(via, mock_user_get_te
|
||||
|
||||
template = factories.TemplateFactory()
|
||||
if via == USER:
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role="member")
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamTemplateAccessFactory(
|
||||
template=template, team="lasuite", role="member"
|
||||
template=template, team="lasuite", role=role
|
||||
)
|
||||
|
||||
other_user = factories.UserFactory()
|
||||
|
||||
for role in [role[0] for role in models.RoleChoices.choices]:
|
||||
for new_role in [role[0] for role in models.RoleChoices.choices]:
|
||||
response = client.post(
|
||||
f"/api/v1.0/templates/{template.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": role,
|
||||
"role": new_role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -456,9 +459,12 @@ def test_api_template_accesses_update_authenticated_unrelated():
|
||||
assert updated_values == old_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_template_accesses_update_authenticated_member(via, mock_user_get_teams):
|
||||
"""Members of a template should not be allowed to update its accesses."""
|
||||
def test_api_template_accesses_update_authenticated_editor_or_reader(
|
||||
via, role, mock_user_get_teams
|
||||
):
|
||||
"""Editors or readers of a template should not be allowed to update its accesses."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -466,11 +472,11 @@ def test_api_template_accesses_update_authenticated_member(via, mock_user_get_te
|
||||
|
||||
template = factories.TemplateFactory()
|
||||
if via == USER:
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role="member")
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamTemplateAccessFactory(
|
||||
template=template, team="lasuite", role="member"
|
||||
template=template, team="lasuite", role=role
|
||||
)
|
||||
|
||||
access = factories.UserTemplateAccessFactory(template=template)
|
||||
@@ -521,14 +527,14 @@ def test_api_template_accesses_update_administrator_except_owner(
|
||||
|
||||
access = factories.UserTemplateAccessFactory(
|
||||
template=template,
|
||||
role=random.choice(["administrator", "member"]),
|
||||
role=random.choice(["administrator", "editor", "reader"]),
|
||||
)
|
||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
||||
|
||||
new_values = {
|
||||
"id": uuid4(),
|
||||
"user_id": factories.UserFactory().id,
|
||||
"role": random.choice(["administrator", "member"]),
|
||||
"role": random.choice(["administrator", "editor", "reader"]),
|
||||
}
|
||||
|
||||
for field, value in new_values.items():
|
||||
@@ -629,7 +635,7 @@ def test_api_template_accesses_update_administrator_to_owner(via, mock_user_get_
|
||||
access = factories.UserTemplateAccessFactory(
|
||||
template=template,
|
||||
user=other_user,
|
||||
role=random.choice(["administrator", "member"]),
|
||||
role=random.choice(["administrator", "editor", "reader"]),
|
||||
)
|
||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
||||
|
||||
@@ -736,7 +742,7 @@ def test_api_template_accesses_update_owner_self(via, mock_user_get_teams):
|
||||
)
|
||||
|
||||
old_values = serializers.TemplateAccessSerializer(instance=access).data
|
||||
new_role = random.choice(["administrator", "member"])
|
||||
new_role = random.choice(["administrator", "editor", "reader"])
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/templates/{template.id!s}/accesses/{access.id!s}/",
|
||||
@@ -797,11 +803,12 @@ def test_api_template_accesses_delete_authenticated():
|
||||
assert models.TemplateAccess.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", ["reader", "editor"])
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_template_accesses_delete_member(via, mock_user_get_teams):
|
||||
def test_api_template_accesses_delete_editor_or_reader(via, role, mock_user_get_teams):
|
||||
"""
|
||||
Authenticated users should not be allowed to delete a template access for a
|
||||
template in which they are a simple member.
|
||||
template in which they are a simple editor or reader.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -810,11 +817,11 @@ def test_api_template_accesses_delete_member(via, mock_user_get_teams):
|
||||
|
||||
template = factories.TemplateFactory()
|
||||
if via == USER:
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role="member")
|
||||
factories.UserTemplateAccessFactory(template=template, user=user, role=role)
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamTemplateAccessFactory(
|
||||
template=template, team="lasuite", role="member"
|
||||
template=template, team="lasuite", role=role
|
||||
)
|
||||
|
||||
access = factories.UserTemplateAccessFactory(template=template)
|
||||
@@ -855,7 +862,7 @@ def test_api_template_accesses_delete_administrators_except_owners(
|
||||
)
|
||||
|
||||
access = factories.UserTemplateAccessFactory(
|
||||
template=template, role=random.choice(["member", "administrator"])
|
||||
template=template, role=random.choice(["reader", "editor", "administrator"])
|
||||
)
|
||||
|
||||
assert models.TemplateAccess.objects.count() == 2
|
||||
|
||||
@@ -15,13 +15,15 @@ def test_api_users_list_anonymous():
|
||||
factories.UserFactory()
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/users/")
|
||||
assert response.status_code == 404
|
||||
assert "Not Found" in response.content.decode("utf-8")
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
def test_api_users_list_authenticated():
|
||||
"""
|
||||
Authenticated users should not be able to list users.
|
||||
Authenticated users should be able to list users.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -32,8 +34,62 @@ def test_api_users_list_authenticated():
|
||||
response = client.get(
|
||||
"/api/v1.0/users/",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert "Not Found" in response.content.decode("utf-8")
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
assert len(content["results"]) == 3
|
||||
|
||||
|
||||
def test_api_users_list_query_email():
|
||||
"""
|
||||
Authenticated users should be able to list users
|
||||
and filter by email.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
dave = factories.UserFactory(email="david.bowman@work.com")
|
||||
nicole = factories.UserFactory(email="nicole_foole@work.com")
|
||||
frank = factories.UserFactory(email="frank_poole@work.com")
|
||||
factories.UserFactory(email="heywood_floyd@work.com")
|
||||
|
||||
response = client.get(
|
||||
"/api/v1.0/users/?q=david.bowman@work.com",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(dave.id)]
|
||||
|
||||
response = client.get("/api/v1.0/users/?q=oole")
|
||||
|
||||
assert response.status_code == 200
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(nicole.id), str(frank.id)]
|
||||
|
||||
|
||||
def test_api_users_list_query_email_exclude_doc_user():
|
||||
"""
|
||||
Authenticated users should be able to list users
|
||||
and filter by email and exclude users who have access to a document.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
nicole = factories.UserFactory(email="nicole_foole@work.com")
|
||||
frank = factories.UserFactory(email="frank_poole@work.com")
|
||||
factories.UserFactory(email="heywood_floyd@work.com")
|
||||
|
||||
factories.UserDocumentAccessFactory(document=document, user=frank)
|
||||
|
||||
response = client.get("/api/v1.0/users/?q=oole&document_id=" + str(document.id))
|
||||
|
||||
assert response.status_code == 200
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(nicole.id)]
|
||||
|
||||
|
||||
def test_api_users_retrieve_me_anonymous():
|
||||
@@ -62,10 +118,7 @@ def test_api_users_retrieve_me_authenticated():
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"id": str(user.id),
|
||||
"language": user.language,
|
||||
"timezone": str(user.timezone),
|
||||
"is_device": False,
|
||||
"is_staff": False,
|
||||
"email": user.email,
|
||||
}
|
||||
|
||||
|
||||
@@ -126,8 +179,10 @@ def test_api_users_create_anonymous():
|
||||
"password": "mypassword",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert "Not Found" in response.content.decode("utf-8")
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
assert models.User.objects.exists() is False
|
||||
|
||||
|
||||
@@ -146,8 +201,8 @@ def test_api_users_create_authenticated():
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert "Not Found" in response.content.decode("utf-8")
|
||||
assert response.status_code == 405
|
||||
assert response.json() == {"detail": 'Method "POST" not allowed.'}
|
||||
assert models.User.objects.exclude(id=user.id).exists() is False
|
||||
|
||||
|
||||
@@ -322,7 +377,7 @@ def test_api_users_delete_list_anonymous():
|
||||
client = APIClient()
|
||||
response = client.delete("/api/v1.0/users/")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == 401
|
||||
assert models.User.objects.count() == 2
|
||||
|
||||
|
||||
@@ -338,7 +393,7 @@ def test_api_users_delete_list_authenticated():
|
||||
"/api/v1.0/users/",
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == 405
|
||||
assert models.User.objects.count() == 3
|
||||
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ def test_models_document_accesses_str():
|
||||
"""
|
||||
user = factories.UserFactory(email="david.bowman@example.com")
|
||||
access = factories.UserDocumentAccessFactory(
|
||||
role="member",
|
||||
role="reader",
|
||||
user=user,
|
||||
document__title="admins",
|
||||
)
|
||||
assert str(access) == "david.bowman@example.com is member in document admins"
|
||||
assert str(access) == "david.bowman@example.com is reader in document admins"
|
||||
|
||||
|
||||
def test_models_document_accesses_unique_user():
|
||||
@@ -87,6 +87,7 @@ def test_models_document_access_get_abilities_anonymous():
|
||||
"destroy": False,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -100,6 +101,7 @@ def test_models_document_access_get_abilities_authenticated():
|
||||
"destroy": False,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -119,7 +121,8 @@ def test_models_document_access_get_abilities_for_owner_of_self_allowed():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["administrator", "member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +136,7 @@ def test_models_document_access_get_abilities_for_owner_of_self_last():
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -149,7 +153,8 @@ def test_models_document_access_get_abilities_for_owner_of_owner():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["administrator", "member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
@@ -165,13 +170,14 @@ def test_models_document_access_get_abilities_for_owner_of_administrator():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["owner", "member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_owner_of_member():
|
||||
"""Check abilities of member access for the owner of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="member")
|
||||
def test_models_document_access_get_abilities_for_owner_of_editor():
|
||||
"""Check abilities of editor access for the owner of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="editor")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="owner"
|
||||
@@ -181,7 +187,25 @@ def test_models_document_access_get_abilities_for_owner_of_member():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["owner", "administrator"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "reader"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_owner_of_reader():
|
||||
"""Check abilities of reader access for the owner of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="owner"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "editor"],
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +224,7 @@ def test_models_document_access_get_abilities_for_administrator_of_owner():
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -216,13 +241,14 @@ def test_models_document_access_get_abilities_for_administrator_of_administrator
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_administrator_of_member():
|
||||
"""Check abilities of member access for the administrator of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="member")
|
||||
def test_models_document_access_get_abilities_for_administrator_of_editor():
|
||||
"""Check abilities of editor access for the administrator of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="editor")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="administrator"
|
||||
@@ -232,53 +258,73 @@ def test_models_document_access_get_abilities_for_administrator_of_member():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["administrator"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "reader"],
|
||||
}
|
||||
|
||||
|
||||
# - for member
|
||||
def test_models_document_access_get_abilities_for_administrator_of_reader():
|
||||
"""Check abilities of reader access for the administrator of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="administrator"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_member_of_owner():
|
||||
"""Check abilities of owner access for the member of a document."""
|
||||
# - for editor
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_editor_of_owner():
|
||||
"""Check abilities of owner access for the editor of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="owner")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="member"
|
||||
document=access.document, role="editor"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_member_of_administrator():
|
||||
"""Check abilities of administrator access for the member of a document."""
|
||||
def test_models_document_access_get_abilities_for_editor_of_administrator():
|
||||
"""Check abilities of administrator access for the editor of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="administrator")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="member"
|
||||
document=access.document, role="editor"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_member_of_member_user(
|
||||
def test_models_document_access_get_abilities_for_editor_of_editor_user(
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of member access for the member of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="member")
|
||||
"""Check abilities of editor access for the editor of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="editor")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="member"
|
||||
document=access.document, role="editor"
|
||||
).user
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
@@ -288,17 +334,77 @@ def test_models_document_access_get_abilities_for_member_of_member_user(
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
# - for reader
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_reader_of_owner():
|
||||
"""Check abilities of owner access for the reader of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="owner")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="reader"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_reader_of_administrator():
|
||||
"""Check abilities of administrator access for the reader of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="administrator")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="reader"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_for_reader_of_reader_user(
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of reader access for the reader of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
factories.UserDocumentAccessFactory(document=access.document) # another one
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="reader"
|
||||
).user
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.get_abilities(user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_document_access_get_abilities_preset_role(django_assert_num_queries):
|
||||
"""No query is done if the role is preset, e.g., with a query annotation."""
|
||||
access = factories.UserDocumentAccessFactory(role="member")
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
user = factories.UserDocumentAccessFactory(
|
||||
document=access.document, role="member"
|
||||
document=access.document, role="reader"
|
||||
).user
|
||||
access.user_roles = ["member"]
|
||||
access.user_roles = ["reader"]
|
||||
|
||||
with django_assert_num_queries(0):
|
||||
abilities = access.get_abilities(user)
|
||||
@@ -307,5 +413,6 @@ def test_models_document_access_get_abilities_preset_role(django_assert_num_quer
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ def test_models_documents_title_max_length():
|
||||
factories.DocumentFactory(title="a" * 256)
|
||||
|
||||
|
||||
def test_models_documents_file_key():
|
||||
"""The file key should be built from the instance uuid."""
|
||||
document = factories.DocumentFactory(id="9531a5f1-42b1-496c-b3f4-1c09ed139b3c")
|
||||
assert document.file_key == "9531a5f1-42b1-496c-b3f4-1c09ed139b3c/file"
|
||||
|
||||
|
||||
# get_abilities
|
||||
|
||||
|
||||
@@ -147,9 +153,28 @@ def test_models_documents_get_abilities_administrator():
|
||||
}
|
||||
|
||||
|
||||
def test_models_documents_get_abilities_member_user(django_assert_num_queries):
|
||||
"""Check abilities returned for the member of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="member")
|
||||
def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"""Check abilities returned for the editor of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="editor")
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.document.get_abilities(access.user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"manage_accesses": False,
|
||||
"partial_update": True,
|
||||
"versions_destroy": False,
|
||||
"versions_list": True,
|
||||
"versions_retrieve": True,
|
||||
}
|
||||
|
||||
|
||||
def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
|
||||
"""Check abilities returned for the reader of a document."""
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.document.get_abilities(access.user)
|
||||
@@ -168,8 +193,8 @@ def test_models_documents_get_abilities_member_user(django_assert_num_queries):
|
||||
|
||||
def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"""No query is done if the role is preset e.g. with query annotation."""
|
||||
access = factories.UserDocumentAccessFactory(role="member")
|
||||
access.document.user_roles = ["member"]
|
||||
access = factories.UserDocumentAccessFactory(role="reader")
|
||||
access.document.user_roles = ["reader"]
|
||||
|
||||
with django_assert_num_queries(0):
|
||||
abilities = access.document.get_abilities(access.user)
|
||||
|
||||
@@ -170,7 +170,7 @@ def test_models_invitation__new_user__user_creation_constant_num_queries(
|
||||
|
||||
def test_models_document_invitations_email():
|
||||
"""Check email invitation during invitation creation."""
|
||||
member_access = factories.UserDocumentAccessFactory(role="member")
|
||||
member_access = factories.UserDocumentAccessFactory(role="reader")
|
||||
document = member_access.document
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
@@ -201,7 +201,7 @@ def test_models_document_invitations_email():
|
||||
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="member")
|
||||
member_access = factories.UserDocumentAccessFactory(role="reader")
|
||||
document = member_access.document
|
||||
|
||||
# pylint: disable-next=no-member
|
||||
@@ -288,17 +288,42 @@ def test_models_document_invitations_get_abilities_privileged_member(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_models_document_invitations_get_abilities_member(via, mock_user_get_teams):
|
||||
"""Check abilities for a document member with 'member' role."""
|
||||
def test_models_document_invitations_get_abilities_reader(via, mock_user_get_teams):
|
||||
"""Check abilities for a document reader with 'reader' role."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="member")
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="member"
|
||||
document=document, team="lasuite", role="reader"
|
||||
)
|
||||
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
abilities = invitation.get_abilities(user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"partial_update": False,
|
||||
"update": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_models_document_invitations_get_abilities_editor(via, mock_user_get_teams):
|
||||
"""Check abilities for a document editor with 'editor' role."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
document = factories.DocumentFactory()
|
||||
if via == USER:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role="editor")
|
||||
elif via == TEAM:
|
||||
mock_user_get_teams.return_value = ["lasuite", "unknown"]
|
||||
factories.TeamDocumentAccessFactory(
|
||||
document=document, team="lasuite", role="editor"
|
||||
)
|
||||
|
||||
invitation = factories.InvitationFactory(document=document)
|
||||
|
||||
@@ -17,11 +17,11 @@ def test_models_template_accesses_str():
|
||||
"""
|
||||
user = factories.UserFactory(email="david.bowman@example.com")
|
||||
access = factories.UserTemplateAccessFactory(
|
||||
role="member",
|
||||
role="reader",
|
||||
user=user,
|
||||
template__title="admins",
|
||||
)
|
||||
assert str(access) == "david.bowman@example.com is member in template admins"
|
||||
assert str(access) == "david.bowman@example.com is reader in template admins"
|
||||
|
||||
|
||||
def test_models_template_accesses_unique_user():
|
||||
@@ -87,6 +87,7 @@ def test_models_template_access_get_abilities_anonymous():
|
||||
"destroy": False,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -100,6 +101,7 @@ def test_models_template_access_get_abilities_authenticated():
|
||||
"destroy": False,
|
||||
"retrieve": False,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -119,7 +121,8 @@ def test_models_template_access_get_abilities_for_owner_of_self_allowed():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["administrator", "member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +136,7 @@ def test_models_template_access_get_abilities_for_owner_of_self_last():
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -149,7 +153,8 @@ def test_models_template_access_get_abilities_for_owner_of_owner():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["administrator", "member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
@@ -165,13 +170,14 @@ def test_models_template_access_get_abilities_for_owner_of_administrator():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["owner", "member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_owner_of_member():
|
||||
"""Check abilities of member access for the owner of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="member")
|
||||
def test_models_template_access_get_abilities_for_owner_of_editor():
|
||||
"""Check abilities of editor access for the owner of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="editor")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="owner"
|
||||
@@ -181,7 +187,25 @@ def test_models_template_access_get_abilities_for_owner_of_member():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["owner", "administrator"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "reader"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_owner_of_reader():
|
||||
"""Check abilities of reader access for the owner of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="reader")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="owner"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["owner", "administrator", "editor"],
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +224,7 @@ def test_models_template_access_get_abilities_for_administrator_of_owner():
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -216,13 +241,14 @@ def test_models_template_access_get_abilities_for_administrator_of_administrator
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["member"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["editor", "reader"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_administrator_of_member():
|
||||
"""Check abilities of member access for the administrator of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="member")
|
||||
def test_models_template_access_get_abilities_for_administrator_of_editor():
|
||||
"""Check abilities of editor access for the administrator of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="editor")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="administrator"
|
||||
@@ -232,53 +258,73 @@ def test_models_template_access_get_abilities_for_administrator_of_member():
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"set_role_to": ["administrator"],
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "reader"],
|
||||
}
|
||||
|
||||
|
||||
# - for member
|
||||
def test_models_template_access_get_abilities_for_administrator_of_reader():
|
||||
"""Check abilities of reader access for the administrator of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="reader")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="administrator"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": True,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"partial_update": True,
|
||||
"set_role_to": ["administrator", "editor"],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_member_of_owner():
|
||||
"""Check abilities of owner access for the member of a template."""
|
||||
# - For editor
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_editor_of_owner():
|
||||
"""Check abilities of owner access for the editor of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="owner")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="member"
|
||||
template=access.template, role="editor"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_member_of_administrator():
|
||||
"""Check abilities of administrator access for the member of a template."""
|
||||
def test_models_template_access_get_abilities_for_editor_of_administrator():
|
||||
"""Check abilities of administrator access for the editor of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="administrator")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="member"
|
||||
template=access.template, role="editor"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_member_of_member_user(
|
||||
def test_models_template_access_get_abilities_for_editor_of_editor_user(
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of member access for the member of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="member")
|
||||
"""Check abilities of editor access for the editor of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="editor")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="member"
|
||||
template=access.template, role="editor"
|
||||
).user
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
@@ -288,17 +334,77 @@ def test_models_template_access_get_abilities_for_member_of_member_user(
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
# - For reader
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_reader_of_owner():
|
||||
"""Check abilities of owner access for the reader of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="owner")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="reader"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_reader_of_administrator():
|
||||
"""Check abilities of administrator access for the reader of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="administrator")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="reader"
|
||||
).user
|
||||
abilities = access.get_abilities(user)
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_for_reader_of_reader_user(
|
||||
django_assert_num_queries
|
||||
):
|
||||
"""Check abilities of reader access for the reader of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="reader")
|
||||
factories.UserTemplateAccessFactory(template=access.template) # another one
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="reader"
|
||||
).user
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.get_abilities(user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
|
||||
def test_models_template_access_get_abilities_preset_role(django_assert_num_queries):
|
||||
"""No query is done if the role is preset, e.g., with a query annotation."""
|
||||
access = factories.UserTemplateAccessFactory(role="member")
|
||||
access = factories.UserTemplateAccessFactory(role="reader")
|
||||
user = factories.UserTemplateAccessFactory(
|
||||
template=access.template, role="member"
|
||||
template=access.template, role="reader"
|
||||
).user
|
||||
access.user_roles = ["member"]
|
||||
access.user_roles = ["reader"]
|
||||
|
||||
with django_assert_num_queries(0):
|
||||
abilities = access.get_abilities(user)
|
||||
@@ -307,5 +413,6 @@ def test_models_template_access_get_abilities_preset_role(django_assert_num_quer
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": False,
|
||||
"partial_update": False,
|
||||
"set_role_to": [],
|
||||
}
|
||||
|
||||
@@ -134,9 +134,26 @@ def test_models_templates_get_abilities_administrator():
|
||||
}
|
||||
|
||||
|
||||
def test_models_templates_get_abilities_member_user(django_assert_num_queries):
|
||||
"""Check abilities returned for the member of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="member")
|
||||
def test_models_templates_get_abilities_editor_user(django_assert_num_queries):
|
||||
"""Check abilities returned for the editor of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="editor")
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.template.get_abilities(access.user)
|
||||
|
||||
assert abilities == {
|
||||
"destroy": False,
|
||||
"retrieve": True,
|
||||
"update": True,
|
||||
"manage_accesses": False,
|
||||
"partial_update": True,
|
||||
"generate_document": True,
|
||||
}
|
||||
|
||||
|
||||
def test_models_templates_get_abilities_reader_user(django_assert_num_queries):
|
||||
"""Check abilities returned for the reader of a template."""
|
||||
access = factories.UserTemplateAccessFactory(role="reader")
|
||||
|
||||
with django_assert_num_queries(1):
|
||||
abilities = access.template.get_abilities(access.user)
|
||||
@@ -153,8 +170,8 @@ def test_models_templates_get_abilities_member_user(django_assert_num_queries):
|
||||
|
||||
def test_models_templates_get_abilities_preset_role(django_assert_num_queries):
|
||||
"""No query is done if the role is preset e.g. with query annotation."""
|
||||
access = factories.UserTemplateAccessFactory(role="member")
|
||||
access.template.user_roles = ["member"]
|
||||
access = factories.UserTemplateAccessFactory(role="reader")
|
||||
access.template.user_roles = ["reader"]
|
||||
|
||||
with django_assert_num_queries(0):
|
||||
abilities = access.template.get_abilities(access.user)
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
<page size="A4">
|
||||
<div class="header">
|
||||
<image src="https://upload.wikimedia.org/wikipedia/fr/7/72/Logo_du_Gouvernement_de_la_R%C3%A9publique_fran%C3%A7aise_%282020%29.svg"/>
|
||||
<h2 class="header-title">Direction<br/>Interministérielle<br/>du numérique</h2>
|
||||
</div>
|
||||
<div class="second-row">
|
||||
<div class="who-ref">
|
||||
<div class="who">La directrice</div>
|
||||
<p class="ref">Réf: 1200001</p>
|
||||
</div>
|
||||
<div class="date">Paris, le 28/09/2023</div>
|
||||
</div>
|
||||
<div class="third-row">
|
||||
<h4 class="title">Note</h4>
|
||||
<h5 class="subtitle">à Monsieur le Premier Ministre</h5>
|
||||
<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">
|
||||
<div class="object">Objet: Generated PDF</div>
|
||||
<div class="body">{{ body }}</div>
|
||||
</div>
|
||||
</page>
|
||||
</page>
|
||||
|
||||
@@ -2,54 +2,17 @@ body {
|
||||
background: white;
|
||||
font-family: arial
|
||||
}
|
||||
img {
|
||||
width: 5cm;
|
||||
margin-left: -0.4cm;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header-title {
|
||||
text-align: right;
|
||||
margin-top: 3rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.second-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 1.2cm;
|
||||
}
|
||||
.ref {
|
||||
margin-top: 0;
|
||||
}
|
||||
.who {
|
||||
font-weight: medium;
|
||||
}
|
||||
.date, .ref {
|
||||
font-size: 12px;
|
||||
}
|
||||
.title, .subtitle {
|
||||
margin: 0;
|
||||
}
|
||||
.subtitle {
|
||||
font-weight: normal;
|
||||
}
|
||||
.object {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1.2cm;
|
||||
margin-top: 3rem
|
||||
.header img {
|
||||
width: 5cm;
|
||||
margin-left: -0.4cm;
|
||||
}
|
||||
.body{
|
||||
margin-top: 1.5rem
|
||||
}
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
p {
|
||||
text-align: justify;
|
||||
ligne-height: 0.8;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Parameters that define how the demo site will be built."""
|
||||
|
||||
NB_OBJECTS = {
|
||||
"users": 50,
|
||||
"docs": 50,
|
||||
"max_users_per_document": 50,
|
||||
}
|
||||
|
||||
DEV_USERS = [
|
||||
{
|
||||
"username": "impress",
|
||||
"email": "impress@impress.world",
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-webkit",
|
||||
"email": "user@webkit.e2e",
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-firefox",
|
||||
"email": "user@firefox.e2e",
|
||||
},
|
||||
{"username": "user-e2e-chromium", "email": "user@chromium.e2e"},
|
||||
]
|
||||
@@ -1,7 +1,6 @@
|
||||
# ruff: noqa: S311, S106
|
||||
"""create_demo management command"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
@@ -15,6 +14,8 @@ from faker import Faker
|
||||
|
||||
from core import models
|
||||
|
||||
from demo import defaults
|
||||
|
||||
fake = Faker()
|
||||
|
||||
logger = logging.getLogger("impress.commands.demo.create_demo")
|
||||
@@ -109,15 +110,94 @@ def create_demo(stdout):
|
||||
|
||||
queue = BulkQueue(stdout)
|
||||
|
||||
with Timeit(stdout, "Creating users"):
|
||||
for i in range(defaults.NB_OBJECTS["users"]):
|
||||
queue.push(
|
||||
models.User(
|
||||
admin_email=f"user{i:d}@example.com",
|
||||
email=f"user{i:d}@example.com",
|
||||
password="!",
|
||||
is_superuser=False,
|
||||
is_active=True,
|
||||
is_staff=False,
|
||||
language=random.choice(settings.LANGUAGES)[0],
|
||||
)
|
||||
)
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating documents"):
|
||||
for _ in range(defaults.NB_OBJECTS["docs"]):
|
||||
queue.push(
|
||||
models.Document(
|
||||
title=fake.sentence(nb_words=4),
|
||||
is_public=random_true_with_probability(0.5),
|
||||
)
|
||||
)
|
||||
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating docs accesses"):
|
||||
docs_ids = list(models.Document.objects.values_list("id", flat=True))
|
||||
users_ids = list(models.User.objects.values_list("id", flat=True))
|
||||
for doc_id in docs_ids:
|
||||
for user_id in random.sample(
|
||||
users_ids,
|
||||
random.randint(1, defaults.NB_OBJECTS["max_users_per_document"]),
|
||||
):
|
||||
role = random.choice(models.RoleChoices.choices)
|
||||
queue.push(
|
||||
models.DocumentAccess(
|
||||
document_id=doc_id, user_id=user_id, role=role[0]
|
||||
)
|
||||
)
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating development users"):
|
||||
for dev_user in defaults.DEV_USERS:
|
||||
queue.push(
|
||||
models.User(
|
||||
admin_email=dev_user["email"],
|
||||
email=dev_user["email"],
|
||||
sub=dev_user["email"],
|
||||
password="!",
|
||||
is_superuser=False,
|
||||
is_active=True,
|
||||
is_staff=False,
|
||||
language=random.choice(settings.LANGUAGES)[0],
|
||||
)
|
||||
)
|
||||
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating docs accesses on development users"):
|
||||
for dev_user in defaults.DEV_USERS:
|
||||
docs_ids = list(models.Document.objects.values_list("id", flat=True))
|
||||
user_id = models.User.objects.get(email=dev_user["email"]).id
|
||||
|
||||
for doc_id in docs_ids:
|
||||
role = random.choice(models.RoleChoices.choices)
|
||||
queue.push(
|
||||
models.DocumentAccess(
|
||||
document_id=doc_id, user_id=user_id, role=role[0]
|
||||
)
|
||||
)
|
||||
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating Template"):
|
||||
with open("demo/data/template/code.txt", "r") as text_file:
|
||||
with open(
|
||||
file="demo/data/template/code.txt", mode="r", encoding="utf-8"
|
||||
) as text_file:
|
||||
code_data = text_file.read()
|
||||
|
||||
with open("demo/data/template/css.txt", "r") as text_file:
|
||||
with open(
|
||||
file="demo/data/template/css.txt", mode="r", encoding="utf-8"
|
||||
) as text_file:
|
||||
css_data = text_file.read()
|
||||
|
||||
queue.push(
|
||||
models.Template(
|
||||
id="baca9e2a-59fb-42ef-b5c6-6f6b05637111",
|
||||
title="Demo Template",
|
||||
description="This is the demo template",
|
||||
code=code_data,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Test the `create_demo` management command"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import override_settings
|
||||
|
||||
@@ -18,3 +16,16 @@ def test_commands_create_demo():
|
||||
call_command("create_demo")
|
||||
|
||||
assert models.Template.objects.count() == 1
|
||||
assert models.User.objects.count() >= 50
|
||||
assert models.Document.objects.count() >= 50
|
||||
assert models.DocumentAccess.objects.count() > 50
|
||||
|
||||
# assert dev users have doc accesses
|
||||
user = models.User.objects.get(email="impress@impress.world")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@webkit.e2e")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@firefox.e2e")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
user = models.User.objects.get(email="user@chromium.e2e")
|
||||
assert models.DocumentAccess.objects.filter(user=user).exists()
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "0.1.0"
|
||||
version = "1.1.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -36,7 +36,7 @@ dependencies = [
|
||||
"django-redis==5.4.0",
|
||||
"django-storages[s3]==1.14.2",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.0.3",
|
||||
"django==5.0.7",
|
||||
"djangorestframework==3.14.0",
|
||||
"drf_spectacular==0.26.5",
|
||||
"dockerflow==2022.8.0",
|
||||
@@ -50,7 +50,7 @@ dependencies = [
|
||||
"psycopg[binary]==3.1.14",
|
||||
"PyJWT==2.8.0",
|
||||
"python-frontmatter==1.0.1",
|
||||
"requests==2.31.0",
|
||||
"requests==2.32.2",
|
||||
"sentry-sdk==1.38.0",
|
||||
"url-normalize==1.4.3",
|
||||
"WeasyPrint>=60.2",
|
||||
|
||||
+10
-1
@@ -39,6 +39,7 @@ COPY ./src/frontend/packages/eslint-config-impress/package.json ./packages/eslin
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY .dockerignore ./.dockerignore
|
||||
COPY ./src/frontend/ .
|
||||
|
||||
### ---- Front-end builder image ----
|
||||
@@ -46,6 +47,14 @@ FROM frontend-deps as impress
|
||||
|
||||
WORKDIR /home/frontend/apps/impress
|
||||
|
||||
FROM frontend-deps as impress-dev
|
||||
|
||||
WORKDIR /home/frontend/apps/impress
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "yarn", "dev"]
|
||||
|
||||
# Tilt will rebuild impress target so, we dissociate impress and impress-builder
|
||||
# to avoid rebuilding the app at every changes.
|
||||
FROM impress as impress-builder
|
||||
@@ -70,4 +79,4 @@ COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
test-results/
|
||||
report/
|
||||
blob-report/
|
||||
playwright/.auth/
|
||||
playwright/.cache/
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
await expect(
|
||||
page.locator('header').first().locator('h2').getByText('Docs'),
|
||||
).toBeVisible();
|
||||
await page.goto('unknown-page404');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { test as setup } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
setup('authenticate-chromium', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, 'chromium');
|
||||
await page
|
||||
.context()
|
||||
.storageState({ path: `playwright/.auth/user-chromium.json` });
|
||||
});
|
||||
|
||||
setup('authenticate-webkit', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, 'webkit');
|
||||
await page
|
||||
.context()
|
||||
.storageState({ path: `playwright/.auth/user-webkit.json` });
|
||||
});
|
||||
@@ -5,14 +5,17 @@ export const keyCloakSignIn = async (page: Page, browserName: string) => {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
if (title?.includes('Sign in to your account')) {
|
||||
await page
|
||||
.getByRole('textbox', { name: 'username' })
|
||||
.fill(`user-e2e-${browserName}`);
|
||||
const login = `user-e2e-${browserName}`;
|
||||
const password = `password-e2e-${browserName}`;
|
||||
|
||||
await page
|
||||
.getByRole('textbox', { name: 'password' })
|
||||
.fill(`password-e2e-${browserName}`);
|
||||
if (await page.getByLabel('Restart login').isVisible()) {
|
||||
await page.getByRole('textbox', { name: 'password' }).fill(password);
|
||||
|
||||
await page.click('input[type="submit"]', { force: true });
|
||||
} else if (title?.includes('Sign in to your account')) {
|
||||
await page.getByRole('textbox', { name: 'username' }).fill(login);
|
||||
|
||||
await page.getByRole('textbox', { name: 'password' }).fill(password);
|
||||
|
||||
await page.click('input[type="submit"]', { force: true });
|
||||
}
|
||||
@@ -23,70 +26,61 @@ export const randomName = (name: string, browserName: string, length: number) =>
|
||||
return `${browserName}-${Math.floor(Math.random() * 10000)}-${index}-${name}`;
|
||||
});
|
||||
|
||||
export const createPad = async (
|
||||
export const createDoc = async (
|
||||
page: Page,
|
||||
padName: string,
|
||||
docName: string,
|
||||
browserName: string,
|
||||
length: number,
|
||||
isPublic: boolean = false,
|
||||
) => {
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
const buttonCreate = page.getByRole('button', { name: 'Create the pad' });
|
||||
|
||||
const randomPads = randomName(padName, browserName, length);
|
||||
|
||||
for (let i = 0; i < randomPads.length; i++) {
|
||||
await panel.getByRole('button', { name: 'Add a pad' }).click();
|
||||
await page.getByText('Pad name').fill(randomPads[i]);
|
||||
await expect(buttonCreate).toBeEnabled();
|
||||
await buttonCreate.click();
|
||||
await expect(panel.locator('li').getByText(randomPads[i])).toBeVisible();
|
||||
}
|
||||
|
||||
return randomPads;
|
||||
};
|
||||
|
||||
export const createTemplate = async (
|
||||
page: Page,
|
||||
templateName: string,
|
||||
browserName: string,
|
||||
length: number,
|
||||
) => {
|
||||
const menu = page.locator('menu').first();
|
||||
await menu.getByLabel(`Template button`).click();
|
||||
|
||||
const panel = page.getByLabel('Templates panel').first();
|
||||
const buttonCreate = page.getByRole('button', {
|
||||
name: 'Create the template',
|
||||
name: 'Create the document',
|
||||
});
|
||||
|
||||
const randomTemplates = randomName(templateName, browserName, length);
|
||||
const randomDocs = randomName(docName, browserName, length);
|
||||
|
||||
for (let i = 0; i < randomDocs.length; i++) {
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
});
|
||||
await buttonCreateHomepage.click();
|
||||
|
||||
// Fill input
|
||||
await page
|
||||
.getByRole('textbox', {
|
||||
name: 'Document name',
|
||||
})
|
||||
.fill(randomDocs[i]);
|
||||
|
||||
if (isPublic) {
|
||||
await page.getByText('Is it public ?').click();
|
||||
}
|
||||
|
||||
for (let i = 0; i < randomTemplates.length; i++) {
|
||||
await panel.getByRole('button', { name: 'Add a template' }).click();
|
||||
await page.getByText('Template name').fill(randomTemplates[i]);
|
||||
await expect(buttonCreate).toBeEnabled();
|
||||
await buttonCreate.click();
|
||||
await expect(
|
||||
panel.locator('li').getByText(randomTemplates[i]),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.locator('h2').getByText(randomDocs[i])).toBeVisible();
|
||||
}
|
||||
|
||||
return randomTemplates;
|
||||
return randomDocs;
|
||||
};
|
||||
|
||||
export const addNewMember = async (
|
||||
page: Page,
|
||||
index: number,
|
||||
role: 'Admin' | 'Owner' | 'Member',
|
||||
fillText: string = 'test',
|
||||
role: 'Administrator' | 'Owner' | 'Member' | 'Editor' | 'Reader',
|
||||
fillText: string = 'user',
|
||||
) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/users/?q=${fillText}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
await page.getByLabel('Add members to the team').click();
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the team/);
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
|
||||
// Select a new user
|
||||
await inputSearch.fill(fillText);
|
||||
@@ -94,23 +88,55 @@ export const addNewMember = async (
|
||||
// Intercept response
|
||||
const responseSearchUser = await responsePromiseSearchUser;
|
||||
const users = (await responseSearchUser.json()).results as {
|
||||
name: string;
|
||||
email: string;
|
||||
}[];
|
||||
|
||||
// Choose user
|
||||
await page.getByRole('option', { name: users[index].name }).click();
|
||||
await page.getByRole('option', { name: users[index].email }).click();
|
||||
|
||||
// Choose a role
|
||||
await page.getByRole('radio', { name: role }).click();
|
||||
|
||||
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||
await page.getByRole('option', { name: role }).click();
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
|
||||
const table = page.getByLabel('List members card').getByRole('table');
|
||||
|
||||
await expect(table.getByText(users[index].name)).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(`Member ${users[index].name} added to the team`),
|
||||
page.getByText(`User ${users[index].email} added to the document.`),
|
||||
).toBeVisible();
|
||||
|
||||
return users[index].name;
|
||||
return users[index].email;
|
||||
};
|
||||
|
||||
interface GoToGridDocOptions {
|
||||
nthRow?: number;
|
||||
title?: string;
|
||||
}
|
||||
export const goToGridDoc = async (
|
||||
page: Page,
|
||||
{ nthRow = 1, title }: GoToGridDocOptions = {},
|
||||
) => {
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
|
||||
const rows = datagrid.getByRole('row');
|
||||
const row = title
|
||||
? rows.filter({
|
||||
hasText: title,
|
||||
})
|
||||
: rows.nth(nthRow);
|
||||
|
||||
const docTitleCell = row.getByRole('cell').nth(1);
|
||||
|
||||
const docTitle = await docTitleCell.textContent();
|
||||
|
||||
expect(docTitle).toBeDefined();
|
||||
|
||||
await docTitleCell.click();
|
||||
|
||||
return docTitle as string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Create', () => {
|
||||
test('checks all the create doc elements are visible', async ({ page }) => {
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
});
|
||||
await buttonCreateHomepage.click();
|
||||
await expect(buttonCreateHomepage).toBeHidden();
|
||||
|
||||
const card = page.getByRole('dialog').first();
|
||||
|
||||
await expect(
|
||||
card.locator('h2').getByText('Create a new document'),
|
||||
).toBeVisible();
|
||||
await expect(card.getByLabel('Document name')).toBeVisible();
|
||||
|
||||
await expect(card.getByText('Is it public ?')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
card.getByRole('button', {
|
||||
name: 'Create the document',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(card.getByLabel('Close the modal')).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the cancel button interaction', async ({ page }) => {
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
});
|
||||
await buttonCreateHomepage.click();
|
||||
await expect(buttonCreateHomepage).toBeHidden();
|
||||
|
||||
const card = page.getByRole('dialog').first();
|
||||
|
||||
await card.getByLabel('Close the modal').click();
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
});
|
||||
|
||||
test('create a new public doc', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'My new doc',
|
||||
browserName,
|
||||
1,
|
||||
true,
|
||||
);
|
||||
|
||||
const header = page.locator('header').first();
|
||||
await header.locator('h2').getByText('Docs').click();
|
||||
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
await expect(datagrid.getByLabel('Loading data')).toBeHidden();
|
||||
|
||||
await expect(datagrid.getByText(docTitle)).toBeVisible();
|
||||
|
||||
const row = datagrid.getByRole('row').filter({
|
||||
hasText: docTitle,
|
||||
});
|
||||
|
||||
await expect(row.getByRole('cell').nth(0)).toHaveText('Public');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
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,
|
||||
}) => {
|
||||
const webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
|
||||
return webSocket.url().includes('ws://localhost:4444/');
|
||||
});
|
||||
|
||||
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||
|
||||
const webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toContain('ws://localhost:4444/');
|
||||
|
||||
const framesentPromise = webSocket.waitForEvent('framesent');
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
const framesent = await framesentPromise;
|
||||
const payload = JSON.parse(framesent.payload as string) as {
|
||||
type: string;
|
||||
};
|
||||
|
||||
const typeCases = ['publish', 'subscribe', 'unsubscribe', 'ping'];
|
||||
expect(typeCases.includes(payload.type)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('markdown button converts from markdown to the editor syntax json', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const randomDoc = await createDoc(page, 'doc-markdown', 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('[test markdown](http://test-markdown.html)');
|
||||
|
||||
await expect(page.getByText('[test markdown]')).toBeVisible();
|
||||
|
||||
await page.getByText('[test markdown]').dblclick();
|
||||
await page.locator('button[data-test="convertMarkdown"]').click();
|
||||
|
||||
await expect(page.getByText('[test markdown]')).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'test markdown',
|
||||
}),
|
||||
).toHaveAttribute('href', 'http://test-markdown.html');
|
||||
});
|
||||
|
||||
test('it renders correctly when we switch from one doc to another', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Check the first doc
|
||||
const firstDoc = await goToGridDoc(page);
|
||||
await expect(page.locator('h2').getByText(firstDoc)).toBeVisible();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World Doc 1');
|
||||
await expect(page.getByText('Hello World Doc 1')).toBeVisible();
|
||||
|
||||
// Check the second doc
|
||||
const secondDoc = await goToGridDoc(page, {
|
||||
nthRow: 2,
|
||||
});
|
||||
await expect(page.locator('h2').getByText(secondDoc)).toBeVisible();
|
||||
await expect(page.getByText('Hello World Doc 1')).toBeHidden();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World Doc 2');
|
||||
await expect(page.getByText('Hello World Doc 2')).toBeVisible();
|
||||
|
||||
// Check the first doc again
|
||||
await goToGridDoc(page, {
|
||||
title: firstDoc,
|
||||
});
|
||||
await expect(page.locator('h2').getByText(firstDoc)).toBeVisible();
|
||||
await expect(page.getByText('Hello World Doc 2')).toBeHidden();
|
||||
await expect(page.getByText('Hello World Doc 1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it saves the doc when we change pages', async ({ page }) => {
|
||||
// Check the first doc
|
||||
const doc = await goToGridDoc(page);
|
||||
await expect(page.locator('h2').getByText(doc)).toBeVisible();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page
|
||||
.locator('.ProseMirror.bn-editor')
|
||||
.fill('Hello World Doc persisted 1');
|
||||
await expect(page.getByText('Hello World Doc persisted 1')).toBeVisible();
|
||||
|
||||
const secondDoc = await goToGridDoc(page, {
|
||||
nthRow: 2,
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByText(`Your document "${doc}" has been saved.`),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.locator('h2').getByText(secondDoc)).toBeVisible();
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: doc,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Hello World Doc persisted 1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it saves the doc when we quit pages', async ({ page, browserName }) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(browserName === 'webkit', 'This test is very flaky with webkit');
|
||||
|
||||
// Check the first doc
|
||||
const doc = await goToGridDoc(page);
|
||||
await expect(page.locator('h2').getByText(doc)).toBeVisible();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page
|
||||
.locator('.ProseMirror.bn-editor')
|
||||
.fill('Hello World Doc persisted 2');
|
||||
await expect(page.getByText('Hello World Doc persisted 2')).toBeVisible();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await goToGridDoc(page, {
|
||||
title: doc,
|
||||
});
|
||||
|
||||
await expect(page.getByText('Hello World Doc persisted 2')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it cannot edit if viewer', 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.getByText(
|
||||
"Read only, you don't have the right to update this document.",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,249 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Documents Grid', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
await expect(page.locator('h2').getByText('Documents')).toBeVisible();
|
||||
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
const thead = datagrid.locator('thead');
|
||||
await expect(thead.getByText(/Document name/i)).toBeVisible();
|
||||
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(/Users number/i)).toBeVisible();
|
||||
|
||||
const row1 = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||
const docName = await row1.nth(1).textContent();
|
||||
expect(docName).toBeDefined();
|
||||
|
||||
const docCreatedAt = await row1.nth(2).textContent();
|
||||
expect(docCreatedAt).toBeDefined();
|
||||
|
||||
const docUpdatedAt = await row1.nth(3).textContent();
|
||||
expect(docUpdatedAt).toBeDefined();
|
||||
|
||||
const docRole = await row1.nth(4).textContent();
|
||||
expect(
|
||||
docRole &&
|
||||
['Administrator', 'Owner', 'Reader', 'Editor'].includes(docRole),
|
||||
).toBeTruthy();
|
||||
|
||||
const docUserNumber = await row1.nth(5).textContent();
|
||||
expect(docUserNumber).toBeDefined();
|
||||
|
||||
// Open the document
|
||||
await row1.nth(1).click();
|
||||
|
||||
await expect(page.locator('h2').getByText(docName!)).toBeVisible();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
nameColumn: 'Document name',
|
||||
ordering: 'title',
|
||||
cellNumber: 1,
|
||||
},
|
||||
{
|
||||
nameColumn: 'Created at',
|
||||
ordering: 'created_at',
|
||||
cellNumber: 2,
|
||||
},
|
||||
{
|
||||
nameColumn: 'Updated at',
|
||||
ordering: 'updated_at',
|
||||
cellNumber: 3,
|
||||
},
|
||||
].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&ordering=-${ordering}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromiseOrderingAsc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1&ordering=${ordering}`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
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 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();
|
||||
|
||||
// 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();
|
||||
|
||||
// Ordering ASC
|
||||
await thead.getByText(nameColumn).click();
|
||||
|
||||
const responseOrderingAsc = await responsePromiseOrderingAsc;
|
||||
expect(responseOrderingAsc.ok()).toBeTruthy();
|
||||
|
||||
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();
|
||||
|
||||
// Ordering Desc
|
||||
await thead.getByText(nameColumn).click();
|
||||
|
||||
const responseOrderingDesc = await responsePromiseOrderingDesc;
|
||||
expect(responseOrderingDesc.ok()).toBeTruthy();
|
||||
|
||||
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();
|
||||
|
||||
expect(
|
||||
textDocNameRow1Desc &&
|
||||
textDocNameRow2Desc &&
|
||||
textDocNameRow1Desc.localeCompare(textDocNameRow2Desc, 'en', {
|
||||
caseFirst: 'false',
|
||||
ignorePunctuation: true,
|
||||
}) >= 0,
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('checks the pagination', async ({ page }) => {
|
||||
const responsePromisePage1 = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=1`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromisePage2 = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/documents/?page=2`) &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const datagridPage1 = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
const responsePage1 = await responsePromisePage1;
|
||||
expect(responsePage1.ok()).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
datagridPage1.getByRole('row').nth(1).getByRole('cell').nth(1),
|
||||
).toHaveText(/.*/);
|
||||
|
||||
await page.getByLabel('Go to page 2').click();
|
||||
|
||||
const datagridPage2 = page
|
||||
.getByLabel('Datagrid of the documents page 2')
|
||||
.getByRole('table');
|
||||
|
||||
const responsePage2 = await responsePromisePage2;
|
||||
expect(responsePage2.ok()).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
datagridPage2.getByRole('row').nth(1).getByRole('cell').nth(1),
|
||||
).toHaveText(/.*/);
|
||||
});
|
||||
|
||||
test('it updates document', async ({ page }) => {
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
const docRow = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||
|
||||
const docName = await docRow.nth(1).textContent();
|
||||
|
||||
await docRow.getByLabel('Open the document options').click();
|
||||
|
||||
await page.getByText('Update document').click();
|
||||
|
||||
await page.getByLabel('Document name').fill(`${docName} updated`);
|
||||
|
||||
await page.getByText('Validate the modification').click();
|
||||
|
||||
await expect(datagrid.getByText(`${docName} updated`)).toBeVisible();
|
||||
});
|
||||
|
||||
test('it deletes the document', async ({ page }) => {
|
||||
const datagrid = page
|
||||
.getByLabel('Datagrid of the documents page 1')
|
||||
.getByRole('table');
|
||||
|
||||
const docRow = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||
|
||||
const docName = await docRow.nth(1).textContent();
|
||||
|
||||
await docRow.getByLabel('Open the document options').click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Delete document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Deleting the document "${docName}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(datagrid.getByText(docName!)).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { goToGridDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Header', () => {
|
||||
test('it checks the element are correctly displayed', 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: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
const card = page.getByLabel(
|
||||
'It is the card information about the document.',
|
||||
);
|
||||
await expect(card.locator('a').getByText('home')).toBeVisible();
|
||||
await expect(card.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||
await expect(card.getByText('Public')).toBeVisible();
|
||||
await expect(
|
||||
card.getByText('Created at 09/01/2021, 11:00 AM'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
card.getByText('Owners: super@owner.com / super2@owner.com'),
|
||||
).toBeVisible();
|
||||
await expect(card.getByText('Your role: Owner')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, randomName } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Document create member', () => {
|
||||
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/users/?q=user') && response.status() === 200,
|
||||
);
|
||||
await createDoc(page, 'select-multi-users', browserName, 1);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
await expect(inputSearch).toBeVisible();
|
||||
|
||||
// Select user 1
|
||||
await inputSearch.fill('user');
|
||||
|
||||
const response = await responsePromise;
|
||||
const users = (await response.json()).results as {
|
||||
email: string;
|
||||
}[];
|
||||
|
||||
await page.getByRole('option', { name: users[0].email }).click();
|
||||
|
||||
// Select user 2
|
||||
await inputSearch.fill('user');
|
||||
await page.getByRole('option', { name: users[1].email }).click();
|
||||
|
||||
// Select email
|
||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||
await inputSearch.fill(email);
|
||||
await page.getByRole('option', { name: email }).click();
|
||||
|
||||
// Check user 1 tag
|
||||
await expect(
|
||||
page.getByText(`${users[0].email}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(`Remove ${users[0].email}`)).toBeVisible();
|
||||
|
||||
// Check user 2 tag
|
||||
await expect(
|
||||
page.getByText(`${users[1].email}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(`Remove ${users[1].email}`)).toBeVisible();
|
||||
|
||||
// Check invitation tag
|
||||
await expect(page.getByText(email, { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel(`Remove ${email}`)).toBeVisible();
|
||||
|
||||
// Check roles are displayed
|
||||
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||
|
||||
await expect(page.getByRole('option', { name: 'Reader' })).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Editor' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('option', { name: 'Administrator' }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('option', { name: 'Owner' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('it sends a new invitation and adds a new user', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/users/?q=user') && response.status() === 200,
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
// Select a new user
|
||||
await inputSearch.fill('user');
|
||||
const responseSearchUser = await responsePromiseSearchUser;
|
||||
const [user] = (await responseSearchUser.json()).results as {
|
||||
email: string;
|
||||
}[];
|
||||
await page.getByRole('option', { name: user.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,
|
||||
);
|
||||
const responsePromiseAddUser = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/accesses/') && 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();
|
||||
|
||||
// Check user added
|
||||
await expect(
|
||||
page.getByText(`User ${user.email} added to the document.`),
|
||||
).toBeVisible();
|
||||
const responseAddUser = await responsePromiseAddUser;
|
||||
expect(responseAddUser.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it try to add twice the same user', async ({ page, browserName }) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/users/?q=user') && response.status() === 200,
|
||||
);
|
||||
|
||||
await createDoc(page, 'user-twice', browserName, 1);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||
await inputSearch.fill('user');
|
||||
const responseSearchUser = await responsePromiseSearchUser;
|
||||
const [user] = (await responseSearchUser.json()).results as {
|
||||
email: string;
|
||||
}[];
|
||||
await page.getByRole('option', { name: user.email }).click();
|
||||
|
||||
// Choose a role
|
||||
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||
await page.getByRole('option', { name: 'Owner' }).click();
|
||||
|
||||
const responsePromiseAddMember = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/accesses/') && response.status() === 201,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Validate' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`User ${user.email} added to the document.`),
|
||||
).toBeVisible();
|
||||
const responseAddMember = await responsePromiseAddMember;
|
||||
expect(responseAddMember.ok()).toBeTruthy();
|
||||
|
||||
await inputSearch.fill('user');
|
||||
await expect(page.getByText('Loading...')).toBeHidden();
|
||||
await expect(page.getByRole('option', { name: user.email })).toBeHidden();
|
||||
});
|
||||
|
||||
test('it try to add twice the same invitation', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
await createDoc(page, 'invitation-twice', 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: 'Owner' }).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();
|
||||
|
||||
await inputSearch.fill(email);
|
||||
await expect(page.getByText('Loading...')).toBeHidden();
|
||||
await expect(page.getByRole('option', { name: email })).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { waitForElementCount } from '../helpers';
|
||||
|
||||
import { addNewMember, createDoc, goToGridDoc } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Document list members', () => {
|
||||
test('it checks a big list of members', async ({ page }) => {
|
||||
await page.route(
|
||||
/.*\/documents\/.*\/accesses\/\?page=.*/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const pageId = url.searchParams.get('page');
|
||||
const accesses = {
|
||||
count: 100,
|
||||
next: 'http://anything/?page=2',
|
||||
previous: null,
|
||||
results: Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`,
|
||||
user: {
|
||||
id: `fc092149-cafa-4ffa-a29d-e4b18af751-${pageId}-${i}`,
|
||||
email: `impress@impress.world-page-${pageId}-${i}`,
|
||||
},
|
||||
team: '',
|
||||
role: 'editor',
|
||||
abilities: {
|
||||
destroy: false,
|
||||
partial_update: true,
|
||||
set_role_to: [],
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: accesses,
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await goToGridDoc(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const list = page.getByLabel('List members card').locator('ul');
|
||||
await expect(list.locator('li')).toHaveCount(20);
|
||||
await list.getByText(`impress@impress.world-page-${1}-18`).hover();
|
||||
await page.mouse.wheel(0, 10);
|
||||
|
||||
await waitForElementCount(list.locator('li'), 21, 10000);
|
||||
|
||||
expect(await list.locator('li').count()).toBeGreaterThan(20);
|
||||
await expect(
|
||||
list.getByText(`impress@impress.world-page-1-16`),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
list.getByText(`impress@impress.world-page-2-15`),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('it checks the role rules', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const list = page.getByLabel('List members card').locator('ul');
|
||||
|
||||
await expect(list.getByText(`user@${browserName}.e2e`)).toBeVisible();
|
||||
|
||||
const soleOwner = list.getByText(
|
||||
`You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`,
|
||||
);
|
||||
|
||||
await expect(soleOwner).toBeVisible();
|
||||
|
||||
const username = await addNewMember(page, 0, 'Owner');
|
||||
|
||||
await expect(list.getByText(username)).toBeVisible();
|
||||
|
||||
await expect(soleOwner).toBeHidden();
|
||||
|
||||
const otherOwner = list.getByText(
|
||||
`You cannot update the role or remove other owner.`,
|
||||
);
|
||||
|
||||
await expect(otherOwner).toBeVisible();
|
||||
|
||||
const SelectRoleCurrentUser = list
|
||||
.locator('li')
|
||||
.filter({
|
||||
hasText: `user@${browserName}.e2e`,
|
||||
})
|
||||
.getByRole('combobox', { name: 'Role' });
|
||||
|
||||
await SelectRoleCurrentUser.click();
|
||||
await page.getByRole('option', { name: 'Administrator' }).click();
|
||||
await expect(page.getByText('The role has been updated')).toBeVisible();
|
||||
|
||||
// Admin still have the right to share
|
||||
await expect(page.locator('h3').getByText('Share')).toBeVisible();
|
||||
|
||||
await SelectRoleCurrentUser.click();
|
||||
await page.getByRole('option', { name: 'Reader' }).click();
|
||||
await expect(page.getByText('The role has been updated')).toBeVisible();
|
||||
|
||||
// Reader does not have the right to share
|
||||
await expect(page.locator('h3').getByText('Share')).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks the delete members', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
const list = page.getByLabel('List members card').locator('ul');
|
||||
|
||||
const nameMyself = `user@${browserName}.e2e`;
|
||||
await expect(list.getByText(nameMyself)).toBeVisible();
|
||||
|
||||
const userOwner = await addNewMember(page, 0, 'Owner');
|
||||
await expect(list.getByText(userOwner)).toBeVisible();
|
||||
|
||||
const userReader = await addNewMember(page, 0, 'Reader');
|
||||
await expect(list.getByText(userReader)).toBeVisible();
|
||||
|
||||
await list
|
||||
.locator('li')
|
||||
.filter({
|
||||
hasText: userReader,
|
||||
})
|
||||
.getByText('delete')
|
||||
.click();
|
||||
|
||||
await expect(list.getByText(userReader)).toBeHidden();
|
||||
|
||||
await list
|
||||
.locator('li')
|
||||
.filter({
|
||||
hasText: nameMyself,
|
||||
})
|
||||
.getByText('delete')
|
||||
.click();
|
||||
|
||||
await expect(list.getByText(nameMyself)).toBeHidden();
|
||||
|
||||
await expect(
|
||||
page.getByText('The member has been removed from the document').first(),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText('Share')).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test.describe('Doc Routing', () => {
|
||||
test('checks alias docs url with homepage', async ({ page }) => {
|
||||
await expect(page).toHaveURL('/');
|
||||
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new document',
|
||||
});
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
|
||||
await page.goto('/docs');
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/docs$/);
|
||||
});
|
||||
|
||||
test('checks 404 on docs/[id] page', async ({ page }) => {
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.goto('/docs/some-unknown-doc');
|
||||
await expect(
|
||||
page.getByText(
|
||||
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',
|
||||
),
|
||||
).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test.describe('Footer', () => {
|
||||
|
||||
@@ -2,9 +2,8 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test.describe('Header', () => {
|
||||
@@ -17,12 +16,12 @@ test.describe('Header', () => {
|
||||
header.getByAltText('Freedom Equality Fraternity Logo'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(header.getByAltText('Impress Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Impress')).toHaveCSS(
|
||||
await expect(header.getByAltText('Docs Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
|
||||
'color',
|
||||
'rgb(0, 0, 145)',
|
||||
);
|
||||
await expect(header.locator('h2').getByText('Impress')).toHaveCSS(
|
||||
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
|
||||
'font-family',
|
||||
/Marianne/i,
|
||||
);
|
||||
@@ -64,8 +63,15 @@ test.describe('Header', () => {
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Header: Log out', () => {
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('checks logout button', async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
|
||||
test('checks logout button', async ({ page }) => {
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'My account',
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test.describe('Language', () => {
|
||||
test('checks the language picker', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Create a new pad',
|
||||
name: 'Create a new document',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -22,7 +19,7 @@ test.describe('Language', () => {
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Créer un nouveau pad',
|
||||
name: 'Créer un nouveau document',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test.describe('Pad Create', () => {
|
||||
test('checks all the create pad elements are visible', async ({ page }) => {
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new pad',
|
||||
});
|
||||
await buttonCreateHomepage.click();
|
||||
await expect(buttonCreateHomepage).toBeHidden();
|
||||
|
||||
const card = page.getByLabel('Create new pad card').first();
|
||||
|
||||
await expect(card.getByLabel('Pad name')).toBeVisible();
|
||||
|
||||
await expect(card.getByLabel('icon group')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
card.getByRole('heading', {
|
||||
name: 'Name the pad',
|
||||
level: 3,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(card.getByText('Is it public ?')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
card.getByRole('button', {
|
||||
name: 'Create the pad',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
card.getByRole('button', {
|
||||
name: 'Cancel',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the cancel button interaction', async ({ page }) => {
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new pad',
|
||||
});
|
||||
await buttonCreateHomepage.click();
|
||||
await expect(buttonCreateHomepage).toBeHidden();
|
||||
|
||||
const card = page.getByLabel('Create new pad card').first();
|
||||
|
||||
await card
|
||||
.getByRole('button', {
|
||||
name: 'Cancel',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the routing on new pad created', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
await panel.getByRole('button', { name: 'Add a pad' }).click();
|
||||
|
||||
const padName = `My routing pad ${browserName}-${Math.floor(Math.random() * 1000)}`;
|
||||
await page.getByText('Pad name').fill(padName);
|
||||
await page.getByRole('button', { name: 'Create the pad' }).click();
|
||||
|
||||
const elPad = page.locator('h2').getByText(padName);
|
||||
await expect(elPad).toBeVisible();
|
||||
|
||||
await panel.getByRole('button', { name: 'Add a pad' }).click();
|
||||
await expect(elPad).toBeHidden();
|
||||
|
||||
await panel.locator('li').getByText(padName).click();
|
||||
await expect(elPad).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks alias pads url with homepage', async ({ page }) => {
|
||||
await expect(page).toHaveURL('/');
|
||||
|
||||
const buttonCreateHomepage = page.getByRole('button', {
|
||||
name: 'Create a new pad',
|
||||
});
|
||||
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
|
||||
await page.goto('/pads');
|
||||
await expect(buttonCreateHomepage).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/pads$/);
|
||||
});
|
||||
|
||||
test('checks 404 on pads/[id] page', async ({ page }) => {
|
||||
await page.goto('/pads/some-unknown-pad');
|
||||
await expect(
|
||||
page.getByText(
|
||||
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',
|
||||
),
|
||||
).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
});
|
||||
|
||||
test('checks that the pad is public', async ({ page, browserName }) => {
|
||||
const responsePromisePad = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/documents/') && response.status() === 201,
|
||||
);
|
||||
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
await panel.getByRole('button', { name: 'Add a pad' }).click();
|
||||
|
||||
const padName = `My routing pad ${browserName}-${Math.floor(Math.random() * 1000)}`;
|
||||
await page.getByText('Pad name').fill(padName);
|
||||
await page.getByText('Is it public ?').click();
|
||||
await page.getByRole('button', { name: 'Create the pad' }).click();
|
||||
|
||||
const responsePad = await responsePromisePad;
|
||||
const is_public = (await responsePad.json()).is_public;
|
||||
expect(is_public).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import cs from 'convert-stream';
|
||||
import pdf from 'pdf-parse';
|
||||
|
||||
import { createPad, keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test.describe('Pad Editor', () => {
|
||||
test('checks the Pad Editor interact correctly', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const randomPad = await createPad(page, 'pad-editor', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomPad[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 Pad is connected to the webrtc server', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
|
||||
return webSocket.url().includes('ws://localhost:4444/');
|
||||
});
|
||||
|
||||
const randomPad = await createPad(page, 'pad-editor', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(randomPad[0])).toBeVisible();
|
||||
|
||||
const webSocket = await webSocketPromise;
|
||||
expect(webSocket.url()).toBe('ws://localhost:4444/');
|
||||
|
||||
const framesentPromise = webSocket.waitForEvent('framesent');
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
const framesent = await framesentPromise;
|
||||
const payload = JSON.parse(framesent.payload as string) as {
|
||||
type: string;
|
||||
};
|
||||
|
||||
const typeCases = ['publish', 'subscribe', 'unsubscribe', 'ping'];
|
||||
expect(typeCases.includes(payload.type)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('markdown button converts from markdown to the editor syntax json', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const randomPad = await createPad(page, 'pad-markdown', browserName, 1);
|
||||
|
||||
await expect(page.locator('h2').getByText(randomPad[0])).toBeVisible();
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page
|
||||
.locator('.ProseMirror.bn-editor')
|
||||
.fill('[test markdown](http://test-markdown.html)');
|
||||
|
||||
await expect(page.getByText('[test markdown]')).toBeVisible();
|
||||
|
||||
await page.getByText('[test markdown]').dblclick();
|
||||
await page.locator('button[data-test="convertMarkdown"]').click();
|
||||
|
||||
await expect(page.getByText('[test markdown]')).toBeHidden();
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'test markdown',
|
||||
}),
|
||||
).toHaveAttribute('href', 'http://test-markdown.html');
|
||||
});
|
||||
|
||||
test('it converts the pad to pdf with a template integrated', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes('impress-document.pdf');
|
||||
});
|
||||
|
||||
const randomPad = await createPad(page, 'pad-editor', browserName, 1);
|
||||
await expect(page.locator('h2').getByText(randomPad[0])).toBeVisible();
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
await page.getByText('Generate PDF').first().click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe('impress-document.pdf');
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfText = (await pdf(pdfBuffer)).text;
|
||||
|
||||
expect(pdfText).toContain('Monsieur le Premier Ministre'); // This is the template text
|
||||
expect(pdfText).toContain('La directrice'); // This is the template text
|
||||
expect(pdfText).toContain('Hello World'); // This is the pad text
|
||||
});
|
||||
|
||||
test('it renders correctly when we switch from one pad to another', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [firstPad, secondPad] = await createPad(
|
||||
page,
|
||||
'pad-multiple',
|
||||
browserName,
|
||||
2,
|
||||
);
|
||||
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
// Check the first pad
|
||||
await panel.getByText(firstPad).click();
|
||||
await expect(page.locator('h2').getByText(firstPad)).toBeVisible();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World Pad 1');
|
||||
await expect(page.getByText('Hello World Pad 1')).toBeVisible();
|
||||
|
||||
// Check the second pad
|
||||
await panel.getByText(secondPad).click();
|
||||
await expect(page.locator('h2').getByText(secondPad)).toBeVisible();
|
||||
await expect(page.getByText('Hello World Pad 1')).toBeHidden();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World Pad 2');
|
||||
await expect(page.getByText('Hello World Pad 2')).toBeVisible();
|
||||
|
||||
// Check the first pad again
|
||||
await panel.getByText(firstPad).click();
|
||||
await expect(page.locator('h2').getByText(firstPad)).toBeVisible();
|
||||
await expect(page.getByText('Hello World Pad 2')).toBeHidden();
|
||||
await expect(page.getByText('Hello World Pad 1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it saves the doc when we change pages', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [pad] = await createPad(page, 'pad-save-page', browserName, 1);
|
||||
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
// Check the first pad
|
||||
await panel.getByText(pad).click();
|
||||
await expect(page.locator('h2').getByText(pad)).toBeVisible();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page
|
||||
.locator('.ProseMirror.bn-editor')
|
||||
.fill('Hello World Pad persisted 1');
|
||||
await expect(page.getByText('Hello World Pad persisted 1')).toBeVisible();
|
||||
|
||||
await panel
|
||||
.getByRole('button', {
|
||||
name: 'Add a pad',
|
||||
})
|
||||
.click();
|
||||
|
||||
const card = page.getByLabel('Create new pad card').first();
|
||||
await expect(
|
||||
card.getByRole('heading', {
|
||||
name: 'Name the pad',
|
||||
level: 3,
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await panel.getByText(pad).click();
|
||||
|
||||
await expect(page.getByText('Hello World Pad persisted 1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it saves the doc when we quit pages', async ({ page, browserName }) => {
|
||||
const [pad] = await createPad(page, 'pad-save-quit', browserName, 1);
|
||||
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
// Check the first pad
|
||||
await panel.getByText(pad).click();
|
||||
await expect(page.locator('h2').getByText(pad)).toBeVisible();
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page
|
||||
.locator('.ProseMirror.bn-editor')
|
||||
.fill('Hello World Pad persisted 2');
|
||||
await expect(page.getByText('Hello World Pad persisted 2')).toBeVisible();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await panel.getByText(pad).click();
|
||||
|
||||
await expect(page.getByText('Hello World Pad persisted 2')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { waitForElementCount } from '../helpers';
|
||||
|
||||
import { createPad, keyCloakSignIn } from './common';
|
||||
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test.describe('Pads Panel', () => {
|
||||
test('checks all the elements are visible', async ({ page }) => {
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
await expect(panel.getByText('Documents')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
panel.getByRole('button', {
|
||||
name: 'Sort the pads',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
panel.getByRole('button', {
|
||||
name: 'Add a pad',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks the sort button', async ({ page }) => {
|
||||
const responsePromiseSortDesc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/documents/?page=1&ordering=-created_at') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const responsePromiseSortAsc = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/documents/?page=1&ordering=created_at') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
await panel
|
||||
.getByRole('button', {
|
||||
name: 'Sort the pads by creation date ascendent',
|
||||
})
|
||||
.click();
|
||||
|
||||
const responseSortAsc = await responsePromiseSortAsc;
|
||||
expect(responseSortAsc.ok()).toBeTruthy();
|
||||
|
||||
await panel
|
||||
.getByRole('button', {
|
||||
name: 'Sort the pads by creation date descendent',
|
||||
})
|
||||
.click();
|
||||
|
||||
const responseSortDesc = await responsePromiseSortDesc;
|
||||
expect(responseSortDesc.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('checks the infinite scroll', async ({ page, browserName }) => {
|
||||
test.setTimeout(90000);
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
|
||||
const randomPads = await createPad(page, 'pad-infinite', browserName, 40);
|
||||
|
||||
await expect(panel.locator('li')).toHaveCount(20);
|
||||
await panel.getByText(randomPads[24]).click();
|
||||
|
||||
await waitForElementCount(panel.locator('li'), 21, 10000);
|
||||
expect(await panel.locator('li').count()).toBeGreaterThan(20);
|
||||
});
|
||||
|
||||
test('checks the hover and selected state', async ({ page, browserName }) => {
|
||||
const panel = page.getByLabel('Pads panel').first();
|
||||
await createPad(page, 'pad-hover', browserName, 2);
|
||||
|
||||
const selectedPad = panel.locator('li').nth(0);
|
||||
await expect(selectedPad).toHaveCSS(
|
||||
'background-color',
|
||||
'rgb(202, 202, 251)',
|
||||
);
|
||||
|
||||
const hoverPad = panel.locator('li').nth(1);
|
||||
await hoverPad.hover();
|
||||
await expect(hoverPad).toHaveCSS('background-color', 'rgb(227, 227, 253)');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
@@ -9,7 +9,7 @@
|
||||
"test:ui": "yarn test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.44.0",
|
||||
"@playwright/test": "1.45.0",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
||||
reporter: [['html', { outputFolder: './report' }]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL: baseURL,
|
||||
baseURL,
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
@@ -42,13 +42,26 @@ export default defineConfig({
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], locale: 'en-US' },
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
locale: 'en-US',
|
||||
timezoneId: 'Europe/Paris',
|
||||
storageState: 'playwright/.auth/user-chromium.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'], locale: 'en-US' },
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
locale: 'en-US',
|
||||
timezoneId: 'Europe/Paris',
|
||||
storageState: 'playwright/.auth/user-webkit.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_API_URL=/api/v1.0/
|
||||
NEXT_PUBLIC_SIGNALING_URL=ws://localhost:4444
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=true
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
NEXT_PUBLIC_API_URL=/api/v1.0/
|
||||
@@ -1,2 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://test.jest
|
||||
NEXT_PUBLIC_API_URL=/api/
|
||||
|
||||
@@ -10,5 +10,5 @@ module.exports = {
|
||||
rootDir: __dirname,
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['node_modules', '.eslintrc.js'],
|
||||
ignorePatterns: ['node_modules', '.eslintrc.js', 'service-worker.js'],
|
||||
};
|
||||
|
||||
@@ -33,3 +33,5 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
service-worker.js
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
next-env.d.ts
|
||||
service-worker.js
|
||||
|
||||
@@ -8,12 +8,8 @@ server {
|
||||
try_files $uri index.html $uri/ =404;
|
||||
}
|
||||
|
||||
location /pads/ {
|
||||
error_page 404 /pads/[id]/;
|
||||
}
|
||||
|
||||
location /templates/ {
|
||||
error_page 404 /templates/[id]/;
|
||||
location /docs/ {
|
||||
error_page 404 /docs/[id]/;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
@@ -344,6 +344,7 @@ const config = {
|
||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||
'value-color': 'var(--c--theme--colors--primary-text)',
|
||||
'font-size': '14px',
|
||||
},
|
||||
'forms-labelledbox': {
|
||||
'label-color': {
|
||||
@@ -351,6 +352,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
'forms-select': {
|
||||
'item-font-size': '14px',
|
||||
'border-radius': '4px',
|
||||
'border-radius-hover': '4px',
|
||||
'background-color': '#ffffff',
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||
|
||||
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
@@ -9,7 +15,11 @@ const nextConfig = {
|
||||
// Enables the styled-components SWC transform
|
||||
styledComponents: true,
|
||||
},
|
||||
webpack(config) {
|
||||
generateBuildId: () => buildId,
|
||||
env: {
|
||||
NEXT_PUBLIC_BUILD_ID: buildId,
|
||||
},
|
||||
webpack(config, { isServer }) {
|
||||
// Grab the existing rule that handles SVG imports
|
||||
const fileLoaderRule = config.module.rules.find((rule) =>
|
||||
rule.test?.test?.('.svg'),
|
||||
@@ -31,6 +41,23 @@ const nextConfig = {
|
||||
},
|
||||
);
|
||||
|
||||
if (!isServer && process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true') {
|
||||
config.plugins.push(
|
||||
new InjectManifest({
|
||||
swSrc: './src/features/service-worker/service-worker.ts',
|
||||
swDest: '../public/service-worker.js',
|
||||
include: [
|
||||
({ asset }) => {
|
||||
if (asset.name.match(/.*(static).*/)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Modify the file loader rule to ignore *.svg, since we have it handled now.
|
||||
fileLoaderRule.exclude = /\.svg$/i;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "prettier --check . && yarn stylelint && next build",
|
||||
"build:ci": "cp .env.development .env.local && yarn build",
|
||||
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes",
|
||||
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes && yarn prettier",
|
||||
"start": "npx -y serve@latest out",
|
||||
"lint": "tsc --noEmit && next lint",
|
||||
"prettier": "prettier --write .",
|
||||
@@ -15,50 +15,52 @@
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "0.13.2",
|
||||
"@blocknote/mantine": "0.13.2",
|
||||
"@blocknote/react": "0.13.2",
|
||||
"@gouvfr-lasuite/integration": "0.1.3",
|
||||
"@openfun/cunningham-react": "2.9.0",
|
||||
"@tanstack/react-query": "5.36.0",
|
||||
"grapesjs-blocks-basic": "1.0.2",
|
||||
"i18next": "23.11.4",
|
||||
"@blocknote/core": "*",
|
||||
"@blocknote/mantine": "*",
|
||||
"@blocknote/react": "*",
|
||||
"@gouvfr-lasuite/integration": "1.0.1",
|
||||
"@openfun/cunningham-react": "2.9.3",
|
||||
"@tanstack/react-query": "5.48.0",
|
||||
"i18next": "23.11.5",
|
||||
"idb": "8.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.4.4",
|
||||
"next": "14.2.3",
|
||||
"next": "14.2.4",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.2.0",
|
||||
"react-aria-components": "1.2.1",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "14.1.1",
|
||||
"react-i18next": "14.1.2",
|
||||
"react-select": "5.8.0",
|
||||
"styled-components": "6.1.11",
|
||||
"y-webrtc": "10.3.0",
|
||||
"yjs": "13.6.15",
|
||||
"zustand": "4.5.2"
|
||||
"yjs": "*",
|
||||
"zustand": "4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.36.0",
|
||||
"@testing-library/jest-dom": "6.4.5",
|
||||
"@testing-library/react": "15.0.7",
|
||||
"@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.1",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "18.3.2",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "*",
|
||||
"cross-env": "*",
|
||||
"dotenv": "16.4.5",
|
||||
"eslint-config-impress": "*",
|
||||
"fetch-mock": "9.11.0",
|
||||
"grapesjs": "0.21.10",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"node-fetch": "2.7.0",
|
||||
"prettier": "3.2.5",
|
||||
"stylelint": "16.5.0",
|
||||
"stylelint-config-standard": "36.0.0",
|
||||
"prettier": "3.3.2",
|
||||
"stylelint": "16.6.1",
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"stylelint-prettier": "5.0.0",
|
||||
"typescript": "*"
|
||||
"typescript": "*",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
id="icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<title>no-image</title>
|
||||
<path
|
||||
d="M30,3.4141,28.5859,2,2,28.5859,3.4141,30l2-2H26a2.0027,2.0027,0,0,0,2-2V5.4141ZM26,26H7.4141l7.7929-7.793,2.3788,2.3787a2,2,0,0,0,2.8284,0L22,19l4,3.9973Zm0-5.8318-2.5858-2.5859a2,2,0,0,0-2.8284,0L19,19.1682l-2.377-2.3771L26,7.4141Z"
|
||||
/>
|
||||
<path
|
||||
d="M6,22V19l5-4.9966,1.3733,1.3733,1.4159-1.416-1.375-1.375a2,2,0,0,0-2.8284,0L6,16.1716V6H22V4H6A2.002,2.002,0,0,0,4,6V22Z"
|
||||
/>
|
||||
<rect
|
||||
id="_Transparent_Rectangle_"
|
||||
data-name="<Transparent Rectangle>"
|
||||
class="cls-1"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 796 B |
@@ -0,0 +1,248 @@
|
||||
<?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"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 311.6 186.5"
|
||||
style="enable-background: new 0 0 311.6 186.5"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.st1 {
|
||||
fill: #000091;
|
||||
}
|
||||
.st2 {
|
||||
fill: #e1000f;
|
||||
}
|
||||
.st3 {
|
||||
fill: #9c9b9b;
|
||||
}
|
||||
</style>
|
||||
<g id="Fond">
|
||||
<rect x="0" class="st0" width="311.6" height="186.5" />
|
||||
</g>
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 279 KiB |
@@ -11,7 +11,7 @@ describe('Page', () => {
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /Create a new pad/i,
|
||||
name: /Create a new document/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -9,15 +9,15 @@ describe('fetchAPI', () => {
|
||||
});
|
||||
|
||||
it('adds correctly the basename', () => {
|
||||
fetchMock.mock('http://test.jest/api/some/url', 200);
|
||||
fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
|
||||
|
||||
void fetchAPI('some/url');
|
||||
|
||||
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/some/url');
|
||||
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v1.0/some/url');
|
||||
});
|
||||
|
||||
it('adds the credentials automatically', () => {
|
||||
fetchMock.mock('http://test.jest/api/some/url', 200);
|
||||
fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
|
||||
|
||||
void fetchAPI('some/url', { body: 'some body' });
|
||||
|
||||
@@ -36,10 +36,18 @@ describe('fetchAPI', () => {
|
||||
.spyOn(useAuthStore.getState(), 'logout')
|
||||
.mockImplementation(logoutMock);
|
||||
|
||||
fetchMock.mock('http://test.jest/api/some/url', 401);
|
||||
fetchMock.mock('http://test.jest/api/v1.0/some/url', 401);
|
||||
|
||||
await fetchAPI('some/url');
|
||||
|
||||
expect(logoutMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('check the versionning', () => {
|
||||
fetchMock.mock('http://test.jest/api/v2.0/some/url', 200);
|
||||
|
||||
void fetchAPI('some/url', {}, '2.0');
|
||||
|
||||
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v2.0/some/url');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,10 +13,12 @@ function getCSRFToken() {
|
||||
.pop();
|
||||
}
|
||||
|
||||
export const fetchAPI = async (input: string, init?: RequestInit) => {
|
||||
const apiUrl = `${baseApiUrl()}${input}`;
|
||||
const { logout } = useAuthStore.getState();
|
||||
|
||||
export const fetchAPI = async (
|
||||
input: string,
|
||||
init?: RequestInit,
|
||||
apiVersion = '1.0',
|
||||
) => {
|
||||
const apiUrl = `${baseApiUrl(apiVersion)}${input}`;
|
||||
const csrfToken = getCSRFToken();
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
@@ -30,6 +32,7 @@ export const fetchAPI = async (input: string, init?: RequestInit) => {
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
const { logout } = useAuthStore.getState();
|
||||
logout();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
@@ -35,6 +35,7 @@ export interface BoxProps {
|
||||
$radius?: CSSProperties['borderRadius'];
|
||||
$transition?: CSSProperties['transition'];
|
||||
$width?: CSSProperties['width'];
|
||||
$wrap?: CSSProperties['flexWrap'];
|
||||
$zIndex?: CSSProperties['zIndex'];
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ export const Box = styled('div')<BoxProps>`
|
||||
${({ $radius }) => $radius && `border-radius: ${$radius};`}
|
||||
${({ $transition }) => $transition && `transition: ${$transition};`}
|
||||
${({ $width }) => $width && `width: ${$width};`}
|
||||
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
|
||||
${({ $css }) => $css && `${$css};`}
|
||||
${({ $zIndex }) => $zIndex && `z-index: ${$zIndex};`}
|
||||
${({ $effect }) => {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Text, TextType } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
interface IconBGProps extends TextType {
|
||||
iconName: string;
|
||||
}
|
||||
|
||||
export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
|
||||
return (
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$size="36px"
|
||||
$theme="primary"
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$css={`border: 1px solid ${colorsTokens()['primary-200']}`}
|
||||
$radius="12px"
|
||||
$padding="4px"
|
||||
$margin="auto"
|
||||
{...textProps}
|
||||
>
|
||||
{iconName}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
interface IconOptionsProps {
|
||||
isOpen: boolean;
|
||||
'aria-label': string;
|
||||
}
|
||||
|
||||
export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => {
|
||||
return (
|
||||
<Text
|
||||
aria-label={props['aria-label']}
|
||||
$isMaterialIcon
|
||||
$css={`
|
||||
transition: all 0.3s ease-in-out;
|
||||
transform: rotate(${isOpen ? '90' : '0'}deg);
|
||||
`}
|
||||
>
|
||||
more_vert
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Text } from '@/components';
|
||||
|
||||
interface IconOptionsProps {
|
||||
isOpen: boolean;
|
||||
'aria-label': string;
|
||||
}
|
||||
|
||||
export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => {
|
||||
return (
|
||||
<Text
|
||||
aria-label={props['aria-label']}
|
||||
className="material-icons"
|
||||
$theme="primary"
|
||||
$css={`
|
||||
transition: all 0.3s ease-in-out;
|
||||
transform: rotate(${isOpen ? '90' : '0'}deg);
|
||||
`}
|
||||
>
|
||||
more_vert
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { ComponentPropsWithRef, PropsWithChildren } from 'react';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
interface SideModalStyleProps {
|
||||
side: 'left' | 'right';
|
||||
width: string;
|
||||
$css?: string;
|
||||
}
|
||||
|
||||
const SideModalStyle = createGlobalStyle<SideModalStyleProps>`
|
||||
@keyframes slidein {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
}
|
||||
|
||||
& .c__modal{
|
||||
animation: slidein 0.7s;
|
||||
|
||||
width: ${({ width }) => width};
|
||||
${({ side }) => side === 'right' && 'left: auto;'};
|
||||
|
||||
.c__modal__scroller {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
${({ $css }) => $css}
|
||||
}
|
||||
`;
|
||||
|
||||
type SideModalType = Omit<ComponentPropsWithRef<typeof Modal>, 'size'>;
|
||||
|
||||
type SideModalProps = SideModalType & Partial<SideModalStyleProps>;
|
||||
|
||||
export const SideModal = ({
|
||||
children,
|
||||
side = 'right',
|
||||
width = '35vw',
|
||||
$css,
|
||||
...modalProps
|
||||
}: PropsWithChildren<SideModalProps>) => {
|
||||
return (
|
||||
<>
|
||||
<SideModalStyle width={width} side={side} $css={$css} />
|
||||
<Modal {...modalProps} size={ModalSize.FULL}>
|
||||
{children}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,8 @@ export interface TextProps extends BoxProps {
|
||||
ReactHTML,
|
||||
'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
>;
|
||||
$elipsis?: boolean;
|
||||
$isMaterialIcon?: boolean;
|
||||
$weight?: CSSProperties['fontWeight'];
|
||||
$textAlign?: CSSProperties['textAlign'];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
@@ -49,12 +51,23 @@ export const TextStyled = styled(Box)<TextProps>`
|
||||
${({ $theme, $variation }) =>
|
||||
`color: var(--c--theme--colors--${$theme}-${$variation});`}
|
||||
${({ $color }) => $color && `color: ${$color};`}
|
||||
${({ $elipsis }) =>
|
||||
$elipsis &&
|
||||
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}
|
||||
`;
|
||||
|
||||
export const Text = ({
|
||||
className,
|
||||
$isMaterialIcon,
|
||||
...props
|
||||
}: ComponentPropsWithRef<typeof TextStyled>) => {
|
||||
return (
|
||||
<TextStyled as="span" $theme="greyscale" $variation="text" {...props} />
|
||||
<TextStyled
|
||||
as="span"
|
||||
$theme="greyscale"
|
||||
$variation="text"
|
||||
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Alert, VariantType } from '@openfun/cunningham-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text, TextType } from '@/components';
|
||||
@@ -5,40 +7,38 @@ import { Box, Text, TextType } from '@/components';
|
||||
interface TextErrorsProps extends TextType {
|
||||
causes?: string[];
|
||||
defaultMessage?: string;
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
export const TextErrors = ({
|
||||
causes,
|
||||
defaultMessage,
|
||||
icon,
|
||||
...textProps
|
||||
}: TextErrorsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{causes &&
|
||||
causes.map((cause, i) => (
|
||||
<Text
|
||||
key={`causes-${i}`}
|
||||
$margin={{ top: 'small' }}
|
||||
$theme="danger"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
{cause}
|
||||
</Text>
|
||||
))}
|
||||
<Alert canClose={false} type={VariantType.ERROR} icon={icon}>
|
||||
<Box $direction="column" $gap="0.2rem">
|
||||
{causes &&
|
||||
causes.map((cause, i) => (
|
||||
<Text
|
||||
key={`causes-${i}`}
|
||||
$theme="danger"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
{cause}
|
||||
</Text>
|
||||
))}
|
||||
|
||||
{!causes && (
|
||||
<Text
|
||||
$margin={{ top: 'small' }}
|
||||
$theme="danger"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
{defaultMessage || t('Something bad happens, please retry.')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{!causes && (
|
||||
<Text $theme="danger" $textAlign="center" {...textProps}>
|
||||
{defaultMessage || t('Something bad happens, please retry.')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@ export * from './Box';
|
||||
export * from './BoxButton';
|
||||
export * from './Card';
|
||||
export * from './DropButton';
|
||||
export * from './IconOptions';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
export * from './Link';
|
||||
export * from './Text';
|
||||
export * from './TextErrors';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CunninghamProvider } from '@openfun/cunningham-react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import '@/i18n/initI18n';
|
||||
@@ -27,7 +26,6 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryDevtools />
|
||||
<CunninghamProvider theme={theme}>
|
||||
<Auth>{children}</Auth>
|
||||
</CunninghamProvider>
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { PropsWithChildren, useEffect, useState } from 'react';
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
import { useAuthStore } from './useAuthStore';
|
||||
export const Auth = ({ children }: PropsWithChildren) => {
|
||||
const { authenticated, initAuth } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const [isDomainComingSoon, setIsDomainComingSoon] = useState(false);
|
||||
const domainComingSoon = 'docs.numerique.gouv.fr';
|
||||
const pathComingSoon = '/coming-soon/';
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (window.location.origin.includes(domainComingSoon)) {
|
||||
router.push(pathComingSoon);
|
||||
return;
|
||||
}
|
||||
|
||||
initAuth();
|
||||
}, [initAuth, router]);
|
||||
}, [initAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDomainComingSoon(
|
||||
window.location.origin.includes(domainComingSoon) &&
|
||||
pathname === pathComingSoon,
|
||||
);
|
||||
}, [pathname]);
|
||||
|
||||
if (!authenticated && !isDomainComingSoon) {
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
|
||||
@@ -8,5 +8,4 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user