Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a073e491b1 | |||
| efd137e328 |
@@ -10,7 +10,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '22.x'
|
||||
node_version: '18.x'
|
||||
with-front-dependencies-installation: true
|
||||
|
||||
synchronize-with-crowdin:
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Create empty source files
|
||||
run: |
|
||||
touch src/backend/locale/django.pot
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
CROWDIN_BASE_PATH: "../src/"
|
||||
# frontend i18n
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
install-dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '22.x'
|
||||
node_version: '18.x'
|
||||
with-front-dependencies-installation: true
|
||||
with-build_mails: true
|
||||
|
||||
@@ -20,19 +20,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
# Backend i18n
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version-file: "src/backend/pyproject.toml"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
python-version: "3.11"
|
||||
- name: Upgrade pip and setuptools
|
||||
run: pip install --upgrade pip setuptools
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .
|
||||
working-directory: src/backend
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -45,10 +45,10 @@ jobs:
|
||||
- name: generate pot files
|
||||
working-directory: src/backend
|
||||
run: |
|
||||
DJANGO_CONFIGURATION=Build uv run python manage.py makemessages -a --keep-pot
|
||||
DJANGO_CONFIGURATION=Build python manage.py makemessages -a --keep-pot
|
||||
# frontend i18n
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
default: '22.x'
|
||||
default: '18.x'
|
||||
type: string
|
||||
with-front-dependencies-installation:
|
||||
type: boolean
|
||||
@@ -20,16 +20,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
- name: Setup Node.js
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
- name: Install dependencies
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
run: cd src/frontend/ && yarn install --frozen-lockfile
|
||||
- name: Cache install frontend
|
||||
if: steps.front-node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/frontend/**/node_modules"
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
@@ -50,10 +50,10 @@ jobs:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Call argocd github webhook
|
||||
run: |
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -58,10 +58,7 @@ jobs:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
- name: create-version-json
|
||||
id: create-version-json
|
||||
uses: jsdaniell/create-json@v1.2.3
|
||||
@@ -85,7 +82,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -102,10 +99,7 @@ jobs:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
dependencies:
|
||||
uses: ./.github/workflows/dependencies.yml
|
||||
with:
|
||||
node_version: '22.x'
|
||||
node_version: '18.x'
|
||||
with-front-dependencies-installation: true
|
||||
with-build_mails: true
|
||||
|
||||
@@ -22,24 +22,20 @@ jobs:
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
run: git log
|
||||
- name: Enforce absence of print statements in code
|
||||
if: always()
|
||||
run: |
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/people.yml' | grep -E "(\bprint\(|\bpprint\()"
|
||||
- name: Check absence of fixup commits
|
||||
if: always()
|
||||
run: |
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install gitlint
|
||||
if: always()
|
||||
run: pip install --user requests gitlint
|
||||
- name: Lint commit messages added to main
|
||||
if: always()
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
@@ -49,17 +45,17 @@ jobs:
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git log --name-only --pretty="" origin/${{ github.event.pull_request.base.ref }}..HEAD | grep CHANGELOG
|
||||
run: git whatchanged --name-only --pretty="" origin/${{ github.event.pull_request.base.ref }}..HEAD | grep CHANGELOG
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
@@ -73,10 +69,10 @@ jobs:
|
||||
needs: dependencies
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
@@ -86,7 +82,7 @@ jobs:
|
||||
run: cd src/frontend/ && yarn ci:build
|
||||
|
||||
- name: Cache build frontend
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: src/frontend/apps/desk/out/
|
||||
key: build-front-${{ github.run_id }}
|
||||
@@ -96,10 +92,10 @@ jobs:
|
||||
needs: dependencies
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
@@ -113,10 +109,10 @@ jobs:
|
||||
needs: dependencies
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
@@ -136,7 +132,7 @@ jobs:
|
||||
shardTotal: [4]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set services env variables
|
||||
run: |
|
||||
@@ -144,7 +140,7 @@ jobs:
|
||||
cat env.d/development/common.e2e.dist >> env.d/development/common
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -152,14 +148,14 @@ jobs:
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Restore the frontend cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: front-node_modules
|
||||
with:
|
||||
path: 'src/frontend/**/node_modules'
|
||||
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
|
||||
|
||||
- name: Restore the build cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: cache-build
|
||||
with:
|
||||
path: src/frontend/apps/desk/out/
|
||||
@@ -220,22 +216,19 @@ jobs:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: "src/backend/pyproject.toml"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
python-version: '3.11'
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run ruff format . --diff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run ruff check .
|
||||
run: ~/.local/bin/ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: uv run pylint .
|
||||
run: ~/.local/bin/pylint .
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -269,35 +262,29 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
sudo mkdir -p /data/media && \
|
||||
sudo mkdir -p /data/static
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: "src/backend/pyproject.toml"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Install the dependencies
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
python-version: '3.11'
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Install gettext (required to compile messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: uv run python manage.py compilemessages
|
||||
|
||||
run: python manage.py compilemessages
|
||||
- name: Run tests
|
||||
run: uv run pytest -n 2
|
||||
run: ~/.local/bin/pytest -n 2
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
creation_rules:
|
||||
- path_regex: ./*
|
||||
key_groups:
|
||||
- age:
|
||||
- age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x # jacques
|
||||
- age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7 # github-repo
|
||||
- age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg # Anthony Le-Courric
|
||||
- age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3 # Antoine Lebaud
|
||||
- age1hnhuzj96ktkhpyygvmz0x9h8mfvssz7ss6emmukags644mdhf4msajk93r # Samuel Paccoud
|
||||
- age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa # Marie Pupo Jeammet
|
||||
- age1rjchule5sncn8r8gfph07muee6vzx4wqfrtldt5jjzke4vlfxy2qqplfvc # Quentin Bey
|
||||
+1
-134
@@ -8,122 +8,6 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.24.0] - 2026-03-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- ✨(mailboxes) enforce lowercase on mailboxes
|
||||
- 🐛(i18n) fix missing translations for status tag labels
|
||||
- 🚚(route) prioritize mail domains as default landing page
|
||||
|
||||
### Changed
|
||||
|
||||
- 💄(config) update ui configuration domain modale
|
||||
- 🍱(static) update logo in invitation email template #1085
|
||||
- ✨(uiV2) use Lasuite UI kit, new layout
|
||||
|
||||
## [1.23.1] - 2026-02-16
|
||||
|
||||
- ✨(invitations) refresh expired invitations
|
||||
|
||||
## [1.23.0] - 2026-02-12
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(demo) add aliases to demo #1050
|
||||
- ✨(front) add icon to button to configure a domain
|
||||
- ✨(datagrid) add sort to mailboxes list + mail domain list
|
||||
- ✨(invitations) allow delete invitations mails domains access by an admin
|
||||
- ✨(front) delete invitations mails domains access
|
||||
- ✨(front) add show invitations mails domains access #1040
|
||||
- ✨(invitations) can delete domain invitations
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(domains) fix attemps to send invitations to existing users #953
|
||||
|
||||
### Changed
|
||||
|
||||
- 🚸(email) we should ignore case when looking for existing emails #1056
|
||||
- 🏗️(core) migrate from pip to uv
|
||||
- ✨(front) add show invitations mails domains access #1040
|
||||
|
||||
## [1.22.2] - 2026-01-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(aliases) authorize special domain devnull in alias destinations #1029
|
||||
|
||||
## [1.22.1] - 2026-01-21
|
||||
|
||||
- 🔒️(organization) the first user is not admin #776
|
||||
- 🐛(admin) fix broken alias import #1021
|
||||
|
||||
## [1.22.0] - 2026-01-19
|
||||
|
||||
### Added
|
||||
- ✨(front) create, manage & delete aliases
|
||||
- ✨(domains) alias sorting and admin
|
||||
- ✨(aliases) delete all aliases in one call #1002
|
||||
|
||||
### Fixed
|
||||
- 🔒️(security) upgrade python version to fix vulnerability #1010
|
||||
- 🐛(dimail) ignore oxadmin when importing mailboxes from dimail #986
|
||||
- ✨(aliases) fix deleting single aliases #1002
|
||||
|
||||
### Changed
|
||||
- 🐛(dimail) allow mailboxes and aliases to have the same local part #986
|
||||
|
||||
### Removed
|
||||
- 🔥(plugins) remove CommuneCreation plugin
|
||||
|
||||
## [1.21.0] - 2025-12-05
|
||||
|
||||
- ✨(aliases) import existing aliases from dimail
|
||||
- 🛂(permissions) return 404 to users with no access to domain #985
|
||||
- ✨(aliases) can create, list and delete aliases #974
|
||||
|
||||
## [1.20.0] - 2025-10-22
|
||||
|
||||
- ✨(models) impose uniqueness on display name, to match ox's constraint
|
||||
- 🐛(dimail) catch duplicate displayname error
|
||||
- ✨(mailbox) synchronize password of newly created mailbox with Dimail's
|
||||
|
||||
## [1.19.1] - 2025-09-19
|
||||
|
||||
- 🐛(fix) add enabled update your mailbox
|
||||
|
||||
## [1.19.0] - 2025-09-03
|
||||
|
||||
- ✨(front) add modal update mailboxes #954
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(api) update mailboxes #934
|
||||
- ✨(api) give update rights to domain viewer on own mailbox #934
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(dimail) grab duplicate displayname error #961
|
||||
|
||||
### Changed
|
||||
|
||||
- 💥(sentry) remove `DJANGO_` before Sentry DSN env variable #957
|
||||
|
||||
## [1.18.2] - 2025-07-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(front) fix missing pagination mail domains #950
|
||||
|
||||
## [1.18.1] - 2025-07-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(front) fix missing pagination on mail domains #946
|
||||
|
||||
## [1.18.0] - 2025-06-30
|
||||
|
||||
### Added
|
||||
|
||||
- 🐛(front) fix missing pagination mail domains
|
||||
@@ -136,10 +20,6 @@ and this project adheres to
|
||||
|
||||
- 🧑💻(docker) split frontend to another file #924
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(webhook) handle user on different home server than room server
|
||||
|
||||
## [1.17.0] - 2025-06-11
|
||||
|
||||
### Added
|
||||
@@ -526,20 +406,7 @@ and this project adheres to
|
||||
- ✨(domains) create and manage domains on admin + API
|
||||
- ✨(domains) mailbox creation + link to email provisioning API
|
||||
|
||||
[unreleased]: https://github.com/suitenumerique/people/compare/v1.24.0...main
|
||||
[1.24.0]: https://github.com/suitenumerique/people/releases/v1.24.0
|
||||
[1.23.1]: https://github.com/suitenumerique/people/releases/v1.23.1
|
||||
[1.23.0]: https://github.com/suitenumerique/people/releases/v1.23.0
|
||||
[1.22.2]: https://github.com/suitenumerique/people/releases/v1.22.2
|
||||
[1.22.1]: https://github.com/suitenumerique/people/releases/v1.22.1
|
||||
[1.22.0]: https://github.com/suitenumerique/people/releases/v1.22.0
|
||||
[1.21.0]: https://github.com/suitenumerique/people/releases/v1.21.0
|
||||
[1.20.0]: https://github.com/suitenumerique/people/releases/v1.20.0
|
||||
[1.19.1]: https://github.com/suitenumerique/people/releases/v1.19.1
|
||||
[1.19.0]: https://github.com/suitenumerique/people/releases/v1.19.0
|
||||
[1.18.2]: https://github.com/suitenumerique/people/releases/v1.18.2
|
||||
[1.18.1]: https://github.com/suitenumerique/people/releases/v1.18.1
|
||||
[1.18.0]: https://github.com/suitenumerique/people/releases/v1.18.0
|
||||
[unreleased]: https://github.com/suitenumerique/people/compare/v1.17.0...main
|
||||
[1.17.0]: https://github.com/suitenumerique/people/releases/v1.17.0
|
||||
[1.16.0]: https://github.com/suitenumerique/people/releases/v1.16.0
|
||||
[1.15.0]: https://github.com/suitenumerique/people/releases/v1.15.0
|
||||
|
||||
+25
-37
@@ -1,7 +1,10 @@
|
||||
# Django People
|
||||
|
||||
# ---- base image to inherit from ----
|
||||
FROM python:3.14.2-alpine AS base
|
||||
FROM python:3.12.6-alpine3.20 AS base
|
||||
|
||||
# Upgrade pip to its latest release to speed up dependencies installation
|
||||
RUN python -m pip install --upgrade pip setuptools
|
||||
|
||||
# Upgrade system packages to install security updates
|
||||
RUN apk update && \
|
||||
@@ -10,30 +13,17 @@ RUN apk update && \
|
||||
# ---- Back-end builder image ----
|
||||
FROM base AS back-builder
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
ENV UV_LINK_MODE=copy
|
||||
WORKDIR /builder
|
||||
|
||||
# Disable Python downloads, because we want to use the system interpreter
|
||||
# across both images. If using a managed Python version, it needs to be
|
||||
# copied from the build image into the final image;
|
||||
ENV UV_PYTHON_DOWNLOADS=0
|
||||
# Copy required python dependencies
|
||||
COPY ./src/backend /builder
|
||||
|
||||
# install uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.10 /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=src/backend/uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=src/backend/pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project --no-dev
|
||||
COPY src/backend /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-dev
|
||||
RUN mkdir /install && \
|
||||
pip install --prefix=/install .
|
||||
|
||||
|
||||
# ---- mails ----
|
||||
FROM node:22 AS mail-builder
|
||||
FROM node:20 AS mail-builder
|
||||
|
||||
COPY ./src/mail /mail/app
|
||||
|
||||
@@ -52,13 +42,14 @@ RUN apk add \
|
||||
pango \
|
||||
rdfind
|
||||
|
||||
# Copy installed python dependencies
|
||||
COPY --from=back-builder /install /usr/local
|
||||
|
||||
# Copy people application (see .dockerignore)
|
||||
COPY ./src/backend /app/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the application from the builder
|
||||
COPY --from=back-builder /app /app
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# collectstatic
|
||||
RUN DJANGO_CONFIGURATION=Build DJANGO_JWT_PRIVATE_SIGNING_KEY=Dummy \
|
||||
python manage.py collectstatic --noinput
|
||||
@@ -89,18 +80,14 @@ COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
# docker user (see entrypoint).
|
||||
RUN chmod g=u /etc/passwd
|
||||
|
||||
# Copy the application from the builder
|
||||
COPY --from=back-builder /app /app
|
||||
# Copy installed python dependencies
|
||||
COPY --from=back-builder /install /usr/local
|
||||
|
||||
# Copy people application (see .dockerignore)
|
||||
COPY ./src/backend /app/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Ensure the uv venv is used at runtime
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Generate compiled translation messages
|
||||
RUN DJANGO_CONFIGURATION=Build \
|
||||
python manage.py compilemessages --ignore=".venv/**/*"
|
||||
|
||||
# We wrap commands run in this container by the following entrypoint that
|
||||
# creates a user on-the-fly with the container user ID (see USER) and root group
|
||||
# ID.
|
||||
@@ -115,9 +102,10 @@ USER root:root
|
||||
# Install psql
|
||||
RUN apk add postgresql-client
|
||||
|
||||
# Install development dependencies
|
||||
RUN --mount=from=ghcr.io/astral-sh/uv:0.9.10,source=/uv,target=/bin/uv \
|
||||
uv sync --all-extras --locked
|
||||
# Uninstall people and re-install it in editable mode along with development
|
||||
# dependencies
|
||||
RUN pip uninstall -y people
|
||||
RUN pip install -e .[dev]
|
||||
|
||||
# Restore the un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
|
||||
@@ -107,8 +107,7 @@ bootstrap: \
|
||||
|
||||
# -- Docker/compose
|
||||
build: ## build the app-dev container
|
||||
@$(COMPOSE) build app-dev frontend-dev
|
||||
@$(COMPOSE) build dimail
|
||||
@$(COMPOSE) build app-dev
|
||||
.PHONY: build
|
||||
|
||||
down: ## stop and remove containers, networks, images, and volumes
|
||||
@@ -124,7 +123,7 @@ run: ## start the wsgi (production) and servers with production Docker images
|
||||
.PHONY: run
|
||||
|
||||
run-dev: ## start the servers in development mode (watch) Docker images
|
||||
@$(COMPOSE) up --force-recreate --detach app-dev dimail frontend-dev celery-dev celery-beat-dev nginx maildev
|
||||
@$(COMPOSE) up --force-recreate --detach app-dev frontend-dev celery-dev celery-beat-dev nginx maildev
|
||||
.PHONY: run-dev
|
||||
|
||||
status: ## an alias for "docker compose ps"
|
||||
@@ -215,7 +214,7 @@ superuser: ## Create an admin superuser with password "admin"
|
||||
.PHONY: superuser
|
||||
|
||||
back-i18n-compile: ## compile the gettext files
|
||||
@$(MANAGE) compilemessages --ignore=".venv/**/*"
|
||||
@$(MANAGE) compilemessages --ignore="venv/**/*"
|
||||
.PHONY: back-i18n-compile
|
||||
|
||||
back-i18n-generate: ## create the .pot files used for i18n
|
||||
@@ -241,21 +240,21 @@ resetdb: ## flush database and create a superuser "admin"
|
||||
.PHONY: resetdb
|
||||
|
||||
env.d/development/common:
|
||||
cp --update=none env.d/development/common.dist env.d/development/common
|
||||
cp -n env.d/development/common.dist env.d/development/common
|
||||
|
||||
env.d/development/france:
|
||||
cp --update=none env.d/development/france.dist env.d/development/france
|
||||
cp -n env.d/development/france.dist env.d/development/france
|
||||
|
||||
env.d/development/postgresql:
|
||||
cp --update=none env.d/development/postgresql.dist env.d/development/postgresql
|
||||
cp -n env.d/development/postgresql.dist env.d/development/postgresql
|
||||
|
||||
env.d/development/kc_postgresql:
|
||||
cp --update=none env.d/development/kc_postgresql.dist env.d/development/kc_postgresql
|
||||
cp -n env.d/development/kc_postgresql.dist env.d/development/kc_postgresql
|
||||
|
||||
# -- Internationalization
|
||||
|
||||
env.d/development/crowdin:
|
||||
cp --update=none env.d/development/crowdin.dist env.d/development/crowdin
|
||||
cp -n env.d/development/crowdin.dist env.d/development/crowdin
|
||||
|
||||
crowdin-download: ## Download translated message from Crowdin
|
||||
@$(COMPOSE_RUN_CROWDIN) download -c crowdin/config.yml
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# People
|
||||
|
||||
People is an application to handle users and teams, and distribute permissions across [La Suite](https://lasuite.numerique.gouv.fr/).
|
||||
People is an application to handle users and teams, and distribute permissions accross [La Suite](https://lasuite.numerique.gouv.fr/).
|
||||
|
||||
It is built on top of [Django Rest
|
||||
Framework](https://www.django-rest-framework.org/).
|
||||
|
||||
+4
-4
@@ -42,11 +42,12 @@ services:
|
||||
- ./src/backend:/app
|
||||
- ./data/media:/data/media
|
||||
- ./data/static:/data/static
|
||||
- /app/.venv
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
dimail:
|
||||
condition: service_started
|
||||
maildev:
|
||||
condition: service_started
|
||||
redis:
|
||||
@@ -118,7 +119,6 @@ services:
|
||||
- ./src/backend:/app
|
||||
- ./data/media:/data/media
|
||||
- ./data/static:/data/static
|
||||
- /app/.venv
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
@@ -199,7 +199,7 @@ services:
|
||||
working_dir: /app
|
||||
|
||||
node:
|
||||
image: node:22
|
||||
image: node:18
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
@@ -259,7 +259,7 @@ services:
|
||||
|
||||
dimail:
|
||||
entrypoint: /opt/dimail-api/start-dev.sh
|
||||
image: registry.mim-libre.fr/dimail/dimail-api:v0.5.2
|
||||
image: registry.mim-libre.fr/dimail/dimail-api:v0.2.11
|
||||
pull_policy: always
|
||||
environment:
|
||||
DIMAIL_MODE: FAKE
|
||||
|
||||
+23
-27
@@ -17,52 +17,48 @@ Whenever we are cooking a new release (e.g. `4.18.1`) we should follow a standar
|
||||
|
||||
This script will ask you for the version you want to release and the kind of release (patch, minor, major). It will then:
|
||||
|
||||
- Create a new branch named: `release/4.18.1`.
|
||||
1. Create a new branch named: `release/4.18.1`.
|
||||
|
||||
- Bump the release number for backend and frontend project.
|
||||
2. Bump the release number for backend and frontend project.
|
||||
|
||||
- Update the project's `Changelog` following the [keepachangelog](https://keepachangelog.com/en/0.3.0/) recommendations
|
||||
3. Update the project's `Changelog` following the [keepachangelog](https://keepachangelog.com/en/0.3.0/) recommendations
|
||||
|
||||
- Commit your changes with the following format: 🔖 release emoji, type of release (patch/minor/patch) and release version:
|
||||
4. Commit your changes with the following format: the 🔖 release emoji, the type of release (patch/minor/patch) and the release version:
|
||||
|
||||
```text
|
||||
🔖(minor) release version 4.18.1
|
||||
```
|
||||
|
||||
- Triggers Crowdin translation action and open related PR
|
||||
3. Open a pull request ask you to wait for an approval from your peers and merge it.
|
||||
|
||||
- Open release PR. Wait for an approval from your peers and merge it.
|
||||
|
||||
> [!NOTE]
|
||||
> It also open the PR for pre-prod deployment, see following section.
|
||||
|
||||
3. Following release script instructions,
|
||||
|
||||
- merge translation and release pulls requests
|
||||
|
||||
- tag and push your commit:
|
||||
4. Ask you to tag and push your commit:
|
||||
|
||||
```bash
|
||||
git tag v4.18.1 && git push origin tag v4.18.1
|
||||
```
|
||||
|
||||
This triggers the CI building new Docker images. You can ensure images were successfully built on Docker Hub [here for back-end](https://hub.docker.com/r/lasuite/people-backend/tags) and [here for front-end](https://hub.docker.com/r/lasuite/people-frontend/tags).
|
||||
Doing this triggers the CI and tells it to build the new Docker image versions that you targeted earlier in the Helm files.
|
||||
|
||||
5. Ensure the new [backend](https://hub.docker.com/r/lasuite/people-backend/tags) and [frontend](https://hub.docker.com/r/lasuite/people-frontend/tags) image tags are on Docker Hub.
|
||||
|
||||
# Deploying
|
||||
6. Create a PR on the [lasuite-deploiement](https://github.com/numerique-gouv/lasuite-deploiement) repository to bump the preprod version.
|
||||
|
||||
## Staging
|
||||
7. The release is now done!
|
||||
|
||||
The `staging` platform is deployed automatically with every update of the `main` branch.
|
||||
# Deploying
|
||||
|
||||
## Pre-prod and production
|
||||
> [!TIP]
|
||||
> The `staging` platform is deployed automatically with every update of the `main` branch.
|
||||
|
||||
If you used the release script and had permission to push on [lasuite-deploiement](https://github.com/numerique-gouv/lasuite-deploiement), a deployment branch has been created. You can skip step 1.
|
||||
Making a new release doesn't publish it automatically in production.
|
||||
|
||||
Otherwise, for manual preprod and for production deployments :
|
||||
1. Bump tag version for both front-end and back-end images in the `.gotmpl` file located in `manifests/<your-product>/env.d/<your-environment>/`,
|
||||
2. Add optional new secrets and variables, if applicable
|
||||
3. Create a pull request
|
||||
4. Submit to approval and merge PR
|
||||
Deployment is done by ArgoCD. ArgoCD checks for the `production` tag and automatically deploys the production platform with the targeted commit.
|
||||
|
||||
The release is now done! 🎉
|
||||
To publish, we mark the commit we want with the `production` tag. ArgoCD is then notified that the tag has changed. It then deploys the Docker image tags specified in the Helm files of the targeted commit.
|
||||
|
||||
To publish the release you just made:
|
||||
|
||||
```bash
|
||||
git tag --force production v4.18.1
|
||||
git push --force origin production
|
||||
```
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
INSTALLED_PLUGINS=plugins.la_suite
|
||||
DNS_PROVISIONING_TARGET_ZONE=test.collectivite.fr
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""People custom admin site."""
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Custom Django admin site application configuration."""
|
||||
|
||||
from django.contrib.admin.apps import AdminConfig
|
||||
|
||||
|
||||
class PeopleAdminConfig(AdminConfig):
|
||||
"""Declare our custom Django admin site."""
|
||||
|
||||
default_site = "admin.sites.PeopleAdminSite"
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Custom Django admin site for the People app."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
class PeopleAdminSite(admin.AdminSite):
|
||||
"""People custom admin site."""
|
||||
|
||||
def each_context(self, request):
|
||||
"""Add custom context to the admin site."""
|
||||
return super().each_context(request) | {
|
||||
"ADMIN_HEADER_BACKGROUND": settings.ADMIN_HEADER_BACKGROUND,
|
||||
"ADMIN_HEADER_COLOR": settings.ADMIN_HEADER_COLOR,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}
|
||||
<style>
|
||||
html[data-theme="light"], :root {
|
||||
{% if ADMIN_HEADER_BACKGROUND %}--header-bg: {{ ADMIN_HEADER_BACKGROUND }};{% endif %}
|
||||
{% if ADMIN_HEADER_COLOR %}--header-color: {{ ADMIN_HEADER_COLOR }};{% endif %}
|
||||
}
|
||||
ul.messagelist li.info {
|
||||
background-color: #EEEEEE;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -621,7 +621,6 @@ class StatView(views.APIView):
|
||||
status=enums.MailDomainStatusChoices.ENABLED
|
||||
).count(),
|
||||
"mailboxes": domains_models.Mailbox.objects.count(),
|
||||
"aliases": domains_models.Alias.objects.count(),
|
||||
}
|
||||
return response.Response(context)
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ from core.models import (
|
||||
AccountService,
|
||||
Contact,
|
||||
Organization,
|
||||
OrganizationAccess,
|
||||
OrganizationRoleChoices,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -129,6 +131,15 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
|
||||
|
||||
user = super().create_user(claims | {"organization": organization})
|
||||
|
||||
if organization_created:
|
||||
# Warning: we may remove this behavior in the near future when we
|
||||
# add a feature to claim the organization ownership.
|
||||
OrganizationAccess.objects.create(
|
||||
organization=organization,
|
||||
user=user,
|
||||
role=OrganizationRoleChoices.ADMIN,
|
||||
)
|
||||
|
||||
# Initiate the user's profile
|
||||
Contact.objects.create(
|
||||
owner=user,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
"""
|
||||
Custom exceptions for mailbox manager app
|
||||
"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class EmailAlreadyKnownException(APIException):
|
||||
"""Exception raised when trying to create a user with an already existing email address."""
|
||||
|
||||
status_code = status.HTTP_201_CREATED
|
||||
default_detail = _(
|
||||
"Email already known. Invitation not sent but access created instead."
|
||||
)
|
||||
default_code = "email already known"
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: S311
|
||||
"""
|
||||
Core application factories
|
||||
"""
|
||||
|
||||
@@ -11,7 +11,7 @@ def update_team_paths(apps, schema_editor):
|
||||
steplen = 5
|
||||
|
||||
# Initialize NumConv with the specified custom alphabet
|
||||
converter = NumConv(alphabet)
|
||||
converter = NumConv(len(alphabet), alphabet)
|
||||
|
||||
nodes = Team.objects.all().order_by("created_at")
|
||||
for i, node in enumerate(nodes, 1):
|
||||
|
||||
@@ -32,7 +32,6 @@ from timezone_field import TimeZoneField
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||
|
||||
from core.enums import WebhookProtocolChoices, WebhookStatusChoices
|
||||
from core.exceptions import EmailAlreadyKnownException
|
||||
from core.plugins.registry import registry as plugin_hooks_registry
|
||||
from core.utils.webhooks import webhooks_synchronizer
|
||||
from core.validators import get_field_validators_from_setting
|
||||
@@ -814,7 +813,7 @@ class Team(MP_Node, BaseModel):
|
||||
except AttributeError:
|
||||
try:
|
||||
role = self.accesses.filter(user=user).values("role")[0]["role"]
|
||||
except TeamAccess.DoesNotExist, IndexError:
|
||||
except (TeamAccess.DoesNotExist, IndexError):
|
||||
role = None
|
||||
|
||||
is_owner_or_admin = role in [RoleChoices.OWNER, RoleChoices.ADMIN]
|
||||
@@ -901,7 +900,7 @@ class TeamAccess(BaseModel):
|
||||
role = self._meta.model.objects.filter(
|
||||
team=self.team_id, user=user
|
||||
).values("role")[0]["role"]
|
||||
except self._meta.model.DoesNotExist, IndexError:
|
||||
except (self._meta.model.DoesNotExist, IndexError):
|
||||
role = None
|
||||
|
||||
is_team_owner_or_admin = role in [RoleChoices.OWNER, RoleChoices.ADMIN]
|
||||
@@ -997,22 +996,19 @@ class BaseInvitation(BaseModel):
|
||||
super().clean()
|
||||
|
||||
# Check if a user already exists for the provided email
|
||||
if User.objects.filter(email__iexact=self.email).exists():
|
||||
raise EmailAlreadyKnownException
|
||||
|
||||
def refresh(self):
|
||||
"""A simple way to refresh invitation and move expiration date."""
|
||||
self.clean()
|
||||
self.updated_at = timezone.now()
|
||||
if User.objects.filter(email=self.email).exists():
|
||||
raise exceptions.ValidationError(
|
||||
{"email": _("This email is already associated to a registered user.")}
|
||||
)
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
"""Calculate if invitation is still valid or has expired."""
|
||||
if not self.updated_at:
|
||||
if not self.created_at:
|
||||
return None
|
||||
|
||||
validity_duration = timedelta(seconds=settings.INVITATION_VALIDITY_DURATION)
|
||||
return timezone.now() > (self.updated_at + validity_duration)
|
||||
return timezone.now() > (self.created_at + validity_duration)
|
||||
|
||||
def _get_mail_subject(self):
|
||||
"""Get the subject of the invitation."""
|
||||
@@ -1100,7 +1096,7 @@ class Invitation(BaseInvitation):
|
||||
role = self.team.accesses.filter(user=user).values("role")[0][
|
||||
"role"
|
||||
]
|
||||
except self._meta.model.DoesNotExist, IndexError:
|
||||
except (self._meta.model.DoesNotExist, IndexError):
|
||||
role = None
|
||||
|
||||
can_delete = role in [RoleChoices.OWNER, RoleChoices.ADMIN]
|
||||
|
||||
@@ -18,7 +18,6 @@ class BasePluginAppConfigMixIn:
|
||||
Initialize the hooks registry when the application is ready.
|
||||
This is called by Django when the application is loaded.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .registry import registry # noqa: PLC0415
|
||||
from .registry import registry # pylint: disable=import-outside-toplevel
|
||||
|
||||
registry.register_app(self.name) # pylint: disable=no-member
|
||||
|
||||
@@ -9,7 +9,7 @@ plugins_urlpatterns = []
|
||||
for app in settings.INSTALLED_PLUGINS:
|
||||
try:
|
||||
plugins_urlpatterns.append(path("", include(f"{app}.urls")))
|
||||
except ImportError, AttributeError:
|
||||
except (ImportError, AttributeError):
|
||||
# Skip if app doesn't have urls.py
|
||||
continue
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -100,7 +100,7 @@ def test_authentication_getter_existing_user_change_fields(
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(2):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
@@ -160,8 +160,7 @@ def test_authentication_getter_existing_user_via_email(
|
||||
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
with django_assert_num_queries(5):
|
||||
# 5 = user by email + user by sub + update sub + 2 from django-lasuite
|
||||
with django_assert_num_queries(3): # user by email + user by sub + update sub
|
||||
user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
@@ -391,8 +390,6 @@ def test_authentication_getter_new_user_with_registration_id_new_organization(
|
||||
assert user.organization.domain_list == expected_domain_list
|
||||
assert user.organization.registration_id_list == expected_registration_id_list
|
||||
|
||||
assert models.OrganizationAccess.objects.filter(user=user).exists() is False
|
||||
|
||||
|
||||
def test_authentication_getter_existing_user_via_email_update_organization(
|
||||
django_assert_num_queries, monkeypatch
|
||||
|
||||
+2
-50
@@ -3,54 +3,6 @@
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
# SEARCH
|
||||
def mock_search_empty():
|
||||
"""Mock response when no Matrix user has been found through search."""
|
||||
return {
|
||||
"message": {"limited": "false", "results": []},
|
||||
"status_code": status.HTTP_200_OK,
|
||||
}
|
||||
|
||||
|
||||
def mock_search_successful(user):
|
||||
"""Mock response when exactly one user has been found through search."""
|
||||
return {
|
||||
"message": {
|
||||
"limited": "false",
|
||||
"results": [
|
||||
{
|
||||
"user_id": f"@{user.email.replace('@', '-')}:user_server.com",
|
||||
"display_name": f"@{user.name} [Fake]",
|
||||
"avatar_url": "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
"status_code": status.HTTP_200_OK,
|
||||
}
|
||||
|
||||
|
||||
def mock_search_successful_multiple(user):
|
||||
"""Mock response when more than one user has been found through search."""
|
||||
return {
|
||||
"message": {
|
||||
"limited": "false",
|
||||
"results": [
|
||||
{
|
||||
"user_id": f"@{user.email.replace('@', '-')}:user_server1.com",
|
||||
"display_name": f"@{user.name} [Fake]",
|
||||
"avatar_url": "null",
|
||||
},
|
||||
{
|
||||
"user_id": f"@{user.email.replace('@', '-')}:user_server2.com",
|
||||
"display_name": f"@{user.name} [Other Fake]",
|
||||
"avatar_url": "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
"status_code": status.HTTP_200_OK,
|
||||
}
|
||||
|
||||
|
||||
# JOIN ROOMS
|
||||
def mock_join_room_successful(room_id):
|
||||
"""Mock response when succesfully joining room. Same response if already in room."""
|
||||
@@ -87,7 +39,7 @@ def mock_invite_user_already_in_room(user):
|
||||
return {
|
||||
"message": {
|
||||
"errcode": "M_FORBIDDEN",
|
||||
"error": f"{user.email.replace('@', '-')}:home_server.fr is already in the room.",
|
||||
"error": f"{user.email.replace('@', ':')} is already in the room.",
|
||||
},
|
||||
"status_code": status.HTTP_403_FORBIDDEN,
|
||||
}
|
||||
@@ -104,7 +56,7 @@ def mock_kick_user_forbidden(user):
|
||||
return {
|
||||
"message": {
|
||||
"errcode": "M_FORBIDDEN",
|
||||
"error": f"You cannot kick user @{user.email.replace('@', '-')}.",
|
||||
"error": f"You cannot kick user @{user.email.replace('@', ':')}.",
|
||||
},
|
||||
"status_code": status.HTTP_403_FORBIDDEN,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from joserfc import jwe as jose_jwe
|
||||
from joserfc import jwt as jose_jwt
|
||||
from joserfc.jwk import RSAKey
|
||||
from joserfc.rfc7518.rsa_key import RSAKey
|
||||
from jwt.utils import to_base64url_uint
|
||||
from lasuite.oidc_resource_server.authentication import (
|
||||
ResourceServerAuthentication,
|
||||
|
||||
@@ -236,73 +236,6 @@ def test_api_team_accesses_create__with_scim_webhook():
|
||||
assert models.TeamAccess.objects.filter(user=other_user, team=team).exists()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_team_accesses_create__with_matrix_webhook():
|
||||
"""
|
||||
If a team has a Matrix webhook, creating a team access should fire a call
|
||||
with the expected payload.
|
||||
"""
|
||||
user, other_user = factories.UserFactory.create_batch(2)
|
||||
|
||||
team = factories.TeamFactory(users=[(user, "owner")])
|
||||
webhook = factories.TeamWebhookFactory(
|
||||
team=team,
|
||||
url="https://server.fr/#/room/room_id:room_server.fr",
|
||||
secret="some-secret-you-should-not-store-on-a-postit",
|
||||
protocol=enums.WebhookProtocolChoices.MATRIX,
|
||||
)
|
||||
|
||||
role = random.choice([role[0] for role in models.RoleChoices.choices])
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Mock successful responses by matrix server
|
||||
responses.post(
|
||||
re.compile(r".*/join"),
|
||||
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||
status=matrix.mock_join_room_successful("room_id")["status_code"],
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(other_user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/invite"),
|
||||
body=str(matrix.mock_invite_successful()["message"]),
|
||||
status=matrix.mock_invite_successful()["status_code"],
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
assert len(responses.calls) == 3
|
||||
assert (
|
||||
responses.calls[0].request.url
|
||||
== "https://room_server.fr/_matrix/client/v3/rooms/room_id:room_server.fr/join"
|
||||
)
|
||||
|
||||
# Payload sent to matrix server
|
||||
assert webhook.secret in responses.calls[0].request.headers["Authorization"]
|
||||
assert json.loads(responses.calls[2].request.body) == {
|
||||
"user_id": f"@{other_user.email.replace('@', '-')}:user_server.com",
|
||||
"reason": f"User added to team {webhook.team} on People",
|
||||
}
|
||||
|
||||
assert models.TeamAccess.objects.filter(user=other_user, team=team).exists()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_team_accesses_create__multiple_webhooks_success(caplog):
|
||||
"""
|
||||
When the team has multiple webhooks, creating a team access should fire all the expected calls.
|
||||
@@ -318,7 +251,7 @@ def test_api_team_accesses_create__multiple_webhooks_success(caplog):
|
||||
)
|
||||
webhook_matrix = factories.TeamWebhookFactory(
|
||||
team=team,
|
||||
url="https://www.webhookserver.fr/#/room/room_id:home_server.fr/",
|
||||
url="https://www.webhookserver.fr/#/room/room_id:home_server/",
|
||||
protocol=enums.WebhookProtocolChoices.MATRIX,
|
||||
secret="yo",
|
||||
)
|
||||
@@ -328,40 +261,39 @@ def test_api_team_accesses_create__multiple_webhooks_success(caplog):
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
# Ensure successful response by scim provider using "responses":
|
||||
responses.patch(
|
||||
re.compile(r".*/Groups/.*"),
|
||||
body="{}",
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/join"),
|
||||
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/invite"),
|
||||
body=str(matrix.mock_invite_successful()["message"]),
|
||||
status=matrix.mock_invite_successful()["status_code"],
|
||||
content_type="application/json",
|
||||
)
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response by scim provider using "responses":
|
||||
rsps.add(
|
||||
rsps.PATCH,
|
||||
re.compile(r".*/Groups/.*"),
|
||||
body="{}",
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/join"),
|
||||
body=str(matrix.mock_join_room_successful),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(r".*/invite"),
|
||||
body=str(matrix.mock_invite_successful()["message"]),
|
||||
status=matrix.mock_invite_successful()["status_code"],
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
response = client.post(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/",
|
||||
{
|
||||
"user": str(other_user.id),
|
||||
"role": role,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Logger
|
||||
log_messages = [msg.message for msg in caplog.records]
|
||||
|
||||
@@ -11,6 +11,7 @@ from rest_framework.test import APIClient
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import factories as domains_factories
|
||||
from mailbox_manager import models as domains_models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -20,20 +21,18 @@ def test_api_stats__anonymous(django_assert_num_queries):
|
||||
|
||||
domains_factories.MailDomainEnabledFactory.create_batch(5)
|
||||
core_factories.TeamFactory.create_batch(3)
|
||||
domains_factories.AliasFactory.create_batch(2)
|
||||
|
||||
# clear cache to allow stats count
|
||||
cache.clear()
|
||||
with django_assert_num_queries(6):
|
||||
with django_assert_num_queries(5):
|
||||
response = APIClient().get("/api/v1.0/stats/")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"total_users": 0,
|
||||
"mau": 0,
|
||||
"active_domains": 7,
|
||||
"active_domains": 5,
|
||||
"mailboxes": 0,
|
||||
"teams": 3,
|
||||
"aliases": 2,
|
||||
}
|
||||
# no new request made due to caching
|
||||
with django_assert_num_queries(0):
|
||||
@@ -42,10 +41,9 @@ def test_api_stats__anonymous(django_assert_num_queries):
|
||||
assert response.json() == {
|
||||
"total_users": 0,
|
||||
"mau": 0,
|
||||
"active_domains": 7,
|
||||
"active_domains": 5,
|
||||
"mailboxes": 0,
|
||||
"teams": 3,
|
||||
"aliases": 2,
|
||||
}
|
||||
|
||||
|
||||
@@ -60,9 +58,10 @@ def test_api_stats__expected_count():
|
||||
|
||||
core_factories.TeamFactory.create_batch(3)
|
||||
domains_factories.MailDomainFactory.create_batch(1)
|
||||
enabled_domain, _ = domains_factories.MailDomainEnabledFactory.create_batch(2)
|
||||
domains_factories.MailboxFactory.create_batch(10, domain=enabled_domain)
|
||||
domains_factories.AliasFactory.create_batch(2, domain=enabled_domain)
|
||||
domains_factories.MailDomainEnabledFactory.create_batch(2)
|
||||
domains_factories.MailboxFactory.create_batch(
|
||||
10, domain=domains_models.MailDomain.objects.all()[1]
|
||||
)
|
||||
|
||||
# clear cache to allow stats count
|
||||
cache.clear()
|
||||
@@ -74,5 +73,4 @@ def test_api_stats__expected_count():
|
||||
"active_domains": 2,
|
||||
"mailboxes": 10,
|
||||
"teams": 3,
|
||||
"aliases": 2,
|
||||
}
|
||||
|
||||
@@ -171,11 +171,10 @@ def test_api_team_invitations__create__cannot_invite_existing_users():
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert (
|
||||
response.json()["detail"]
|
||||
== "Email already known. Invitation not sent but access created instead."
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["email"] == [
|
||||
"This email is already associated to a registered user."
|
||||
]
|
||||
|
||||
|
||||
def test_api_team_invitations__list__anonymous_user():
|
||||
|
||||
@@ -16,7 +16,6 @@ from faker import Faker
|
||||
from freezegun import freeze_time
|
||||
|
||||
from core import factories, models
|
||||
from core.exceptions import EmailAlreadyKnownException
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -49,17 +48,6 @@ def test_models_invitations_team_required():
|
||||
factories.InvitationFactory(team=None)
|
||||
|
||||
|
||||
def test_models_invitations_email_case_insensitive_duplicate_check():
|
||||
"""The email validation should be case-insensitive when checking for existing users."""
|
||||
# Create a user with a lowercase email
|
||||
factories.UserFactory(email="john.doe@example.com")
|
||||
|
||||
# Try to create an invitation with different case
|
||||
# This should raise the same exception as if the email was exactly the same
|
||||
with pytest.raises(EmailAlreadyKnownException):
|
||||
factories.InvitationFactory(email="John.Doe@Example.COM")
|
||||
|
||||
|
||||
def test_models_invitations_team_should_be_team_instance():
|
||||
"""The "team" field should be a team instance."""
|
||||
with pytest.raises(ValueError, match='Invitation.team" must be a "Team" instance'):
|
||||
|
||||
@@ -13,63 +13,11 @@ from rest_framework import status
|
||||
from core import factories
|
||||
from core.enums import WebhookProtocolChoices
|
||||
from core.tests.fixtures import matrix
|
||||
from core.utils.matrix import MatrixAPIClient
|
||||
from core.utils.webhooks import webhooks_synchronizer
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
## SEARCH
|
||||
@responses.activate
|
||||
def test_matrix_webhook__search_user_unknown(caplog):
|
||||
"""When searching for a user in the Matrix federation but cannot find any,
|
||||
we invite a (future ?) user using user's email and room's server."""
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
user = factories.UserFactory()
|
||||
webhook = factories.TeamWebhookFactory(
|
||||
protocol=WebhookProtocolChoices.MATRIX,
|
||||
url="https://www.matrix.org/#/room/room_id:room_server.au",
|
||||
secret="secret-access-token",
|
||||
)
|
||||
|
||||
client = MatrixAPIClient()
|
||||
# Mock successful responses
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_empty()["message"]),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.get_user_id(user=user, webhook=webhook)
|
||||
assert response == f"@{user.email.replace('@', '-')}:room_server.au"
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_matrix_webhook__search_multiple_ids(caplog):
|
||||
"""When searching for a user in Matrix federation,
|
||||
if user directory returns multiple ids, invite the first one."""
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
user = factories.UserFactory()
|
||||
webhook = factories.TeamWebhookFactory(
|
||||
protocol=WebhookProtocolChoices.MATRIX,
|
||||
url="https://www.matrix.org/#/room/room_id:room_server.au",
|
||||
secret="secret-access-token",
|
||||
)
|
||||
|
||||
client = MatrixAPIClient()
|
||||
# Mock successful responses
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful_multiple(user)["message"]),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.get_user_id(user=user, webhook=webhook)
|
||||
assert response == f"@{user.email.replace('@', '-')}:user_server1.com"
|
||||
|
||||
|
||||
## INVITE
|
||||
@responses.activate
|
||||
def test_matrix_webhook__invite_user_to_room_forbidden(caplog):
|
||||
@@ -90,11 +38,6 @@ def test_matrix_webhook__invite_user_to_room_forbidden(caplog):
|
||||
body=str(matrix.mock_join_room_successful),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/invite"),
|
||||
body=str(error["message"]),
|
||||
@@ -121,11 +64,6 @@ def test_matrix_webhook__invite_user_to_room_already_in_room(caplog):
|
||||
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||
status=matrix.mock_join_room_successful("room_id")["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/invite"),
|
||||
body=str(matrix.mock_invite_user_already_in_room(user)["message"]),
|
||||
@@ -163,11 +101,6 @@ def test_matrix_webhook__invite_user_to_room_success(caplog):
|
||||
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||
status=matrix.mock_join_room_successful("room_id")["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/invite"),
|
||||
body=str(matrix.mock_invite_successful()["message"]),
|
||||
@@ -180,8 +113,8 @@ def test_matrix_webhook__invite_user_to_room_success(caplog):
|
||||
assert webhook.secret in headers["Authorization"]
|
||||
|
||||
# Check payloads sent to Matrix API
|
||||
assert json.loads(responses.calls[2].request.body) == {
|
||||
"user_id": f"@{user.email.replace('@', '-')}:user_server.com",
|
||||
assert json.loads(responses.calls[1].request.body) == {
|
||||
"user_id": f"@{user.email.replace('@', ':')}",
|
||||
"reason": f"User added to team {webhook.team} on People",
|
||||
}
|
||||
|
||||
@@ -198,7 +131,7 @@ def test_matrix_webhook__invite_user_to_room_success(caplog):
|
||||
|
||||
|
||||
@responses.activate
|
||||
@override_settings(MATRIX_BOT_ACCESS_TOKEN="TCHAP_TOKEN")
|
||||
@override_settings(TCHAP_ACCESS_TOKEN="TCHAP_TOKEN")
|
||||
def test_matrix_webhook__override_secret_for_tchap():
|
||||
"""The user passed to the function should get invited."""
|
||||
user = factories.UserFactory()
|
||||
@@ -214,11 +147,6 @@ def test_matrix_webhook__override_secret_for_tchap():
|
||||
body=str(matrix.mock_join_room_successful("room_id")["message"]),
|
||||
status=matrix.mock_join_room_successful("room_id")["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/invite"),
|
||||
body=str(matrix.mock_invite_successful()["message"]),
|
||||
@@ -230,9 +158,6 @@ def test_matrix_webhook__override_secret_for_tchap():
|
||||
headers = responses.calls[0].request.headers
|
||||
assert "TCHAP_TOKEN" in headers["Authorization"]
|
||||
|
||||
# Check correctly inferred base url from room_id
|
||||
assert "matrix.home_server" in responses.calls[0].request.url
|
||||
|
||||
|
||||
## KICK
|
||||
@responses.activate
|
||||
@@ -253,11 +178,6 @@ def test_matrix_webhook__kick_user_from_room_not_in_room(caplog):
|
||||
body=str(matrix.mock_join_room_successful),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/kick"),
|
||||
body=str(matrix.mock_kick_user_not_in_room()["message"]),
|
||||
@@ -285,7 +205,7 @@ def test_matrix_webhook__kick_user_from_room_success(caplog):
|
||||
user = factories.UserFactory()
|
||||
webhook = factories.TeamWebhookFactory(
|
||||
protocol=WebhookProtocolChoices.MATRIX,
|
||||
url="https://www.matrix.org/#/room/room_id:room_server",
|
||||
url="https://www.matrix.org/#/room/room_id:home_server",
|
||||
secret="secret-access-token",
|
||||
)
|
||||
|
||||
@@ -294,11 +214,6 @@ def test_matrix_webhook__kick_user_from_room_success(caplog):
|
||||
body=str(matrix.mock_join_room_successful),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/kick"),
|
||||
body=str(matrix.mock_kick_successful),
|
||||
@@ -307,8 +222,8 @@ def test_matrix_webhook__kick_user_from_room_success(caplog):
|
||||
webhooks_synchronizer.remove_user_from_group(team=webhook.team, user=user)
|
||||
|
||||
# Check payloads sent to Matrix API
|
||||
assert json.loads(responses.calls[2].request.body) == {
|
||||
"user_id": f"@{user.email.replace('@', '-')}:user_server.com",
|
||||
assert json.loads(responses.calls[1].request.body) == {
|
||||
"user_id": f"@{user.email.replace('@', ':')}",
|
||||
"reason": f"User removed from team {webhook.team} on People",
|
||||
}
|
||||
|
||||
@@ -343,11 +258,6 @@ def test_matrix_webhook__kick_user_from_room_forbidden(caplog):
|
||||
body=str(matrix.mock_join_room_successful),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/search"),
|
||||
body=json.dumps(matrix.mock_search_successful(user)["message"]),
|
||||
status=matrix.mock_search_successful(user)["status_code"],
|
||||
)
|
||||
responses.post(
|
||||
re.compile(r".*/kick"),
|
||||
body=str(error["message"]),
|
||||
|
||||
@@ -29,11 +29,13 @@ session.mount("https://", adapter)
|
||||
class MatrixAPIClient:
|
||||
"""A client to interact with Matrix API"""
|
||||
|
||||
secret = settings.TCHAP_ACCESS_TOKEN
|
||||
|
||||
def get_headers(self, webhook):
|
||||
"""Build header dict from webhook object."""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if "tchap.gouv.fr" in webhook.url:
|
||||
token = settings.MATRIX_BOT_ACCESS_TOKEN
|
||||
token = settings.TCHAP_ACCESS_TOKEN
|
||||
elif webhook.secret:
|
||||
token = webhook.secret
|
||||
else:
|
||||
@@ -45,31 +47,16 @@ class MatrixAPIClient:
|
||||
"""Returns room id from webhook url."""
|
||||
room_id = webhook_url.split("/room/")[1]
|
||||
base_url = room_id.split(":")[1]
|
||||
if "tchap.gouv.fr" in webhook_url and "i.tchap.gouv.fr" not in webhook_url:
|
||||
if "tchap.gouv.fr" in webhook_url:
|
||||
base_url = f"matrix.{base_url}"
|
||||
return f"https://{base_url}/_matrix/client/v3/rooms/{room_id}"
|
||||
|
||||
def get_user_id(self, user, webhook):
|
||||
def get_user_id(self, user):
|
||||
"""Returns user id from email."""
|
||||
if user.email is None:
|
||||
raise ValueError("You must first set an email for the user.")
|
||||
|
||||
if settings.MATRIX_BASE_HOME_SERVER:
|
||||
home_server = settings.MATRIX_BASE_HOME_SERVER
|
||||
search = session.post(
|
||||
f"{home_server}/_matrix/client/v3/user_directory/search",
|
||||
json={"search_term": f"@{user.email.replace('@', '-')}"},
|
||||
headers=self.get_headers(webhook),
|
||||
verify=True,
|
||||
timeout=3,
|
||||
)
|
||||
results = search.json()["results"]
|
||||
if len(results) > 0:
|
||||
return results[0]["user_id"]
|
||||
|
||||
# try and invite unknown user using room home server
|
||||
room_home_server = webhook.url.split(":")[2]
|
||||
return f"@{user.email.replace('@', '-')}:{room_home_server}"
|
||||
return f"@{user.email.replace('@', ':')}"
|
||||
|
||||
def join_room(self, webhook):
|
||||
"""Accept invitation to the room. As of today, it is a mandatory step
|
||||
@@ -78,7 +65,7 @@ class MatrixAPIClient:
|
||||
f"{self._get_room_url(webhook.url)}/join",
|
||||
json={},
|
||||
headers=self.get_headers(webhook),
|
||||
verify=True,
|
||||
verify=False,
|
||||
timeout=3,
|
||||
)
|
||||
|
||||
@@ -91,11 +78,8 @@ class MatrixAPIClient:
|
||||
webhook.url,
|
||||
)
|
||||
return join_response, False
|
||||
logger.info(
|
||||
"Succesfully joined room",
|
||||
)
|
||||
|
||||
user_id = self.get_user_id(user, webhook)
|
||||
user_id = self.get_user_id(user)
|
||||
response = session.post(
|
||||
f"{self._get_room_url(webhook.url)}/invite",
|
||||
json={
|
||||
@@ -103,7 +87,7 @@ class MatrixAPIClient:
|
||||
"reason": f"User added to team {webhook.team} on People",
|
||||
},
|
||||
headers=self.get_headers(webhook),
|
||||
verify=True,
|
||||
verify=False,
|
||||
timeout=3,
|
||||
)
|
||||
|
||||
@@ -128,7 +112,7 @@ class MatrixAPIClient:
|
||||
)
|
||||
return join_response, False
|
||||
|
||||
user_id = self.get_user_id(user, webhook)
|
||||
user_id = self.get_user_id(user)
|
||||
response = session.post(
|
||||
f"{self._get_room_url(webhook.url)}/kick",
|
||||
json={
|
||||
@@ -136,7 +120,7 @@ class MatrixAPIClient:
|
||||
"reason": f"User removed from team {webhook.team} on People",
|
||||
},
|
||||
headers=self.get_headers(webhook),
|
||||
verify=True,
|
||||
verify=False,
|
||||
timeout=3,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class WebhookClient:
|
||||
response, webhook_succeeded = self._get_response_and_status(
|
||||
name, webhook, user
|
||||
)
|
||||
|
||||
if response is not None:
|
||||
extra = {"response": response.content}
|
||||
# pylint: disable=no-member
|
||||
|
||||
@@ -4,7 +4,5 @@ NB_OBJECTS = {
|
||||
"users": 1000,
|
||||
"teams": 100,
|
||||
"max_users_per_team": 100,
|
||||
"domains": 10,
|
||||
"mailboxes_per_domain": 2,
|
||||
"aliases_per_domain": 2,
|
||||
"domains": 20,
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ def create_oidc_people_idp_client_user():
|
||||
)
|
||||
|
||||
|
||||
def create_demo(stdout): # pylint: disable=too-many-branches too-many-statements too-many-locals # noqa: PLR0912, PLR0915
|
||||
def create_demo(stdout): # pylint: disable=too-many-locals
|
||||
"""
|
||||
Create a database with demo data for developers to work in a realistic environment.
|
||||
The code is engineered to create a huge number of objects fast.
|
||||
@@ -288,12 +288,14 @@ def create_demo(stdout): # pylint: disable=too-many-branches too-many-statement
|
||||
)
|
||||
queue.flush()
|
||||
|
||||
domains = mailbox_models.MailDomain.objects.all()
|
||||
with Timeit(stdout, "Creating accesses to domains"):
|
||||
for domain in domains:
|
||||
domains_ids = list(
|
||||
mailbox_models.MailDomain.objects.values_list("id", flat=True)
|
||||
)
|
||||
for domain_id in domains_ids:
|
||||
queue.push(
|
||||
mailbox_models.MailDomainAccess(
|
||||
domain=domain,
|
||||
domain_id=domain_id,
|
||||
user_id=random.choice(users_ids),
|
||||
role=models.RoleChoices.OWNER,
|
||||
)
|
||||
@@ -301,40 +303,6 @@ def create_demo(stdout): # pylint: disable=too-many-branches too-many-statement
|
||||
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating mailboxes"):
|
||||
for domain in domains:
|
||||
for i in range(defaults.NB_OBJECTS["mailboxes_per_domain"]):
|
||||
first_name = fake.first_name()
|
||||
last_name = fake.last_name()
|
||||
local_part = f"{first_name.lower()}.{last_name.lower()}{i}"
|
||||
|
||||
queue.push(
|
||||
mailbox_models.Mailbox(
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
local_part=local_part,
|
||||
domain=domain,
|
||||
secondary_email=f"{local_part}@example.fr",
|
||||
status=random.choice(MailboxStatusChoices.values),
|
||||
dn_email=local_part,
|
||||
)
|
||||
)
|
||||
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating aliases"):
|
||||
for domain in domains:
|
||||
for _i in range(defaults.NB_OBJECTS["aliases_per_domain"]):
|
||||
queue.push(
|
||||
mailbox_models.Alias(
|
||||
local_part=fake.word(),
|
||||
destination=fake.email(),
|
||||
domain=domain,
|
||||
)
|
||||
)
|
||||
|
||||
queue.flush()
|
||||
|
||||
with Timeit(stdout, "Creating specific users"):
|
||||
# ⚠️ Warning: this users also need to be created in the keycloak
|
||||
# realm.json AND the OIDC setting to fallback on user email
|
||||
@@ -369,7 +337,7 @@ def create_demo(stdout): # pylint: disable=too-many-branches too-many-statement
|
||||
queue.push(user_with_mail)
|
||||
queue.push(
|
||||
mailbox_models.MailDomainAccess(
|
||||
domain=domains[0],
|
||||
domain_id=domains_ids[0],
|
||||
user_id=user_with_mail.pk,
|
||||
role=role,
|
||||
)
|
||||
@@ -395,7 +363,7 @@ def create_demo(stdout): # pylint: disable=too-many-branches too-many-statement
|
||||
)
|
||||
queue.push(
|
||||
mailbox_models.MailDomainAccess(
|
||||
domain=domains[0],
|
||||
domain_id=domains_ids[0],
|
||||
user_id=team_mail_user.pk,
|
||||
role=domain_role,
|
||||
)
|
||||
@@ -403,62 +371,17 @@ def create_demo(stdout): # pylint: disable=too-many-branches too-many-statement
|
||||
|
||||
queue.flush()
|
||||
|
||||
# Enabled domain for 2E2 tests
|
||||
enabled_domain, _created = mailbox_models.MailDomain.objects.get_or_create(
|
||||
name="enabled-domain.com",
|
||||
status=MailDomainStatusChoices.ENABLED,
|
||||
support_email="support@enabled-domain.com",
|
||||
)
|
||||
domain_owner = models.User.objects.get(email="e2e.mail-owner@example.com")
|
||||
mailbox_models.MailDomainAccess.objects.get_or_create(
|
||||
domain=enabled_domain,
|
||||
user=domain_owner,
|
||||
role=MailDomainRoleChoices.OWNER,
|
||||
)
|
||||
mailbox_models.MailDomainInvitation.objects.create(
|
||||
issuer=domain_owner,
|
||||
domain=enabled_domain,
|
||||
email="people@people.world",
|
||||
role=MailDomainRoleChoices.ADMIN,
|
||||
)
|
||||
|
||||
# Many objects domain
|
||||
many_objects_domain, _created = mailbox_models.MailDomain.objects.get_or_create(
|
||||
name="many-objects-domain.com",
|
||||
status=MailDomainStatusChoices.ENABLED,
|
||||
support_email="support@mbd.com",
|
||||
)
|
||||
mailbox_models.MailDomainAccess.objects.get_or_create(
|
||||
domain=many_objects_domain,
|
||||
user=domain_owner,
|
||||
role=MailDomainRoleChoices.OWNER,
|
||||
)
|
||||
mailbox_models.MailDomainInvitation.objects.create(
|
||||
issuer=domain_owner,
|
||||
domain=many_objects_domain,
|
||||
email="people@people.world",
|
||||
role=MailDomainRoleChoices.OWNER,
|
||||
)
|
||||
for _i in range(30):
|
||||
first_name = fake.first_name()
|
||||
last_name = fake.last_name()
|
||||
local_part = f"{first_name.lower()}.{last_name.lower()}"
|
||||
|
||||
mailbox_models.Mailbox.objects.create(
|
||||
domain=many_objects_domain,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
local_part=local_part,
|
||||
secondary_email=f"{local_part}@example.fr",
|
||||
status=random.choice(MailboxStatusChoices.values),
|
||||
password=make_password(None), # unusable password
|
||||
dn_email=f"{local_part}@{many_objects_domain}",
|
||||
# Enabled domain for 2E2 tests
|
||||
enabled_domain, _created = mailbox_models.MailDomain.objects.get_or_create(
|
||||
name="enabled-domain.com",
|
||||
status=MailDomainStatusChoices.ENABLED,
|
||||
support_email="support@enabled-domain.com",
|
||||
)
|
||||
|
||||
mailbox_models.Alias.objects.create(
|
||||
local_part=fake.word(),
|
||||
destination=fake.email(),
|
||||
domain=many_objects_domain,
|
||||
domain_owner = models.User.objects.get(email="e2e.mail-owner@example.com")
|
||||
mailbox_models.MailDomainAccess.objects.get_or_create(
|
||||
domain=enabled_domain,
|
||||
user=domain_owner,
|
||||
role=MailDomainRoleChoices.OWNER,
|
||||
)
|
||||
|
||||
# OIDC configuration
|
||||
|
||||
@@ -19,9 +19,7 @@ TEST_NB_OBJECTS = {
|
||||
"users": 100,
|
||||
"teams": 100,
|
||||
"max_users_per_team": 5,
|
||||
"domains": 10,
|
||||
"mailboxes_per_domain": 2,
|
||||
"aliases_per_domain": 2,
|
||||
"domains": 100,
|
||||
}
|
||||
|
||||
|
||||
@@ -35,38 +33,25 @@ def test_commands_create_demo(settings):
|
||||
call_command("create_demo")
|
||||
|
||||
# Monique Test, Jeanne Test and Jean Something (quick fix for e2e)
|
||||
# 3 users with team rights
|
||||
# 3 users with domain rights
|
||||
# 3 user with team rights
|
||||
# 3 user with domain rights
|
||||
# 3 x 3 user with both rights
|
||||
assert models.User.objects.count() == TEST_NB_OBJECTS["users"] + 3 + 3 + 3 + 9
|
||||
|
||||
assert models.Team.objects.count() == TEST_NB_OBJECTS["teams"]
|
||||
assert models.TeamAccess.objects.count() >= TEST_NB_OBJECTS["teams"]
|
||||
|
||||
# nb_domains + example.com + enabled + many-boxed-domain
|
||||
assert (
|
||||
mailbox_models.MailDomain.objects.count()
|
||||
== TEST_NB_OBJECTS["domains"] + 1 + 1 + 1
|
||||
mailbox_models.MailDomain.objects.count() == TEST_NB_OBJECTS["domains"] + 1 + 1
|
||||
)
|
||||
|
||||
# 3 domain access for each user with domain rights
|
||||
# 3 x 3 domain access for each user with both rights
|
||||
# 2 domains for E2E mail owner user
|
||||
# 1 domain for E2E mail owner user
|
||||
assert (
|
||||
mailbox_models.MailDomainAccess.objects.count()
|
||||
== TEST_NB_OBJECTS["domains"] + 3 + 9 + 2
|
||||
== TEST_NB_OBJECTS["domains"] + 3 + 9 + 1
|
||||
)
|
||||
|
||||
# TEST_NB_OBJECTS["domains"]*TEST_NB_OBJECTS["mailboxes_per_domain"] = 20
|
||||
# + 30 in the many-object-domain
|
||||
# + 1 mailbox for user-e2e@example.com
|
||||
# = 51
|
||||
assert mailbox_models.Mailbox.objects.count() == 51
|
||||
|
||||
# TEST_NB_OBJECTS["domains"]*TEST_NB_OBJECTS["mailboxes_per_alias"] = 20
|
||||
# + 30 in the many-object-domain
|
||||
assert mailbox_models.Alias.objects.count() == 50
|
||||
|
||||
|
||||
def test_commands_createsuperuser():
|
||||
"""
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-13 15:21+0000\n"
|
||||
"PO-Revision-Date: 2026-03-24 08:54\n"
|
||||
"POT-Creation-Date: 2025-06-10 16:13+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 09:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -17,336 +17,559 @@ msgstr ""
|
||||
"X-Crowdin-File: backend.pot\n"
|
||||
"X-Crowdin-File-ID: 2\n"
|
||||
|
||||
#: core/admin.py:62
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:73
|
||||
#: build/lib/core/admin.py:73 core/admin.py:73
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:85
|
||||
#: build/lib/core/admin.py:85 core/admin.py:85
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:124
|
||||
#: build/lib/core/admin.py:124 core/admin.py:124
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:226
|
||||
#: build/lib/core/admin.py:226 core/admin.py:226
|
||||
msgid "Run post creation plugins"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:234
|
||||
#: build/lib/core/admin.py:234 core/admin.py:234
|
||||
msgid "Post creation plugins have been run for the selected organizations."
|
||||
msgstr ""
|
||||
|
||||
#: core/apps.py:65
|
||||
#: build/lib/core/apps.py:65 core/apps.py:65
|
||||
msgid "People core application"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:102
|
||||
#: build/lib/core/authentication/backends.py:104
|
||||
#: core/authentication/backends.py:104
|
||||
msgid "Claims contained no recognizable user identification"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:122
|
||||
#: build/lib/core/authentication/backends.py:124
|
||||
#: core/authentication/backends.py:124
|
||||
msgid "Claims contained no recognizable organization identification"
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:170 core/authentication/backends.py:172
|
||||
#: build/lib/core/authentication/backends.py:181
|
||||
#: build/lib/core/authentication/backends.py:183
|
||||
#: core/authentication/backends.py:181 core/authentication/backends.py:183
|
||||
msgid "Invalid authorization header."
|
||||
msgstr ""
|
||||
|
||||
#: core/authentication/backends.py:177
|
||||
#: build/lib/core/authentication/backends.py:188
|
||||
#: core/authentication/backends.py:188
|
||||
msgid "Invalid api key."
|
||||
msgstr ""
|
||||
|
||||
#: core/enums.py:24
|
||||
#: build/lib/core/enums.py:24 core/enums.py:24
|
||||
msgid "Failure"
|
||||
msgstr ""
|
||||
|
||||
#: core/enums.py:25 mailbox_manager/enums.py:21 mailbox_manager/enums.py:31
|
||||
#: build/lib/core/enums.py:25 build/lib/mailbox_manager/enums.py:21
|
||||
#: build/lib/mailbox_manager/enums.py:31 core/enums.py:25
|
||||
#: mailbox_manager/enums.py:21 mailbox_manager/enums.py:31
|
||||
msgid "Pending"
|
||||
msgstr ""
|
||||
|
||||
#: core/enums.py:26
|
||||
#: build/lib/core/enums.py:26 core/enums.py:26
|
||||
msgid "Success"
|
||||
msgstr ""
|
||||
|
||||
#: core/exceptions.py:16
|
||||
msgid "Email already known. Invitation not sent but access created instead."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:77
|
||||
#: build/lib/core/models.py:76 core/models.py:76
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:78 core/models.py:90 mailbox_manager/enums.py:14
|
||||
#: build/lib/core/models.py:77 build/lib/core/models.py:89
|
||||
#: build/lib/mailbox_manager/enums.py:14 core/models.py:77 core/models.py:89
|
||||
#: mailbox_manager/enums.py:14
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:79 mailbox_manager/enums.py:15
|
||||
#: build/lib/core/models.py:78 build/lib/mailbox_manager/enums.py:15
|
||||
#: core/models.py:78 mailbox_manager/enums.py:15
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:102
|
||||
#: build/lib/core/models.py:101 core/models.py:101
|
||||
msgid "id"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:103
|
||||
#: build/lib/core/models.py:102 core/models.py:102
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:109
|
||||
#: build/lib/core/models.py:108 core/models.py:108
|
||||
msgid "created at"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:110
|
||||
#: build/lib/core/models.py:109 core/models.py:109
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:115
|
||||
#: build/lib/core/models.py:114 core/models.py:114
|
||||
msgid "updated at"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:116
|
||||
#: build/lib/core/models.py:115 core/models.py:115
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:155
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:156
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:159
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
msgid "notes"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:161
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "contact information"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:162
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
msgid "A JSON object containing the contact information"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:176
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "contact"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:177
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "contacts"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:251 core/models.py:365 core/models.py:511 core/models.py:1125
|
||||
#: mailbox_manager/models.py:56
|
||||
#: build/lib/core/models.py:250 build/lib/core/models.py:364
|
||||
#: build/lib/core/models.py:510 build/lib/core/models.py:1106
|
||||
#: build/lib/mailbox_manager/models.py:53 core/models.py:250 core/models.py:364
|
||||
#: core/models.py:510 core/models.py:1106 mailbox_manager/models.py:53
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:253
|
||||
#: build/lib/core/models.py:252 core/models.py:252
|
||||
msgid "audience id"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:258
|
||||
#: build/lib/core/models.py:257 core/models.py:257
|
||||
msgid "service provider"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:259
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
msgid "service providers"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:373
|
||||
#: build/lib/core/models.py:372 core/models.py:372
|
||||
msgid "registration ID list"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:380
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
msgid "domain list"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:387
|
||||
#: build/lib/core/models.py:386 core/models.py:386
|
||||
msgid "metadata"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:388
|
||||
#: build/lib/core/models.py:387 core/models.py:387
|
||||
msgid "A JSON object containing the organization metadata"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:398 core/models.py:536
|
||||
#: build/lib/core/models.py:397 build/lib/core/models.py:535 core/models.py:397
|
||||
#: core/models.py:535
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:404
|
||||
#: build/lib/core/models.py:403 core/models.py:403
|
||||
msgid "organization"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:405
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "organizations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:412
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "An organization must have at least a registration ID or a domain."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:496
|
||||
#: build/lib/core/models.py:495 core/models.py:495
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:502
|
||||
#: build/lib/core/models.py:501 core/models.py:501
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:504
|
||||
#: build/lib/core/models.py:503 core/models.py:503
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:510 core/models.py:974
|
||||
#: build/lib/core/models.py:509 build/lib/core/models.py:958 core/models.py:509
|
||||
#: core/models.py:958
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:516
|
||||
#: build/lib/core/models.py:515 core/models.py:515
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:517
|
||||
#: build/lib/core/models.py:516 core/models.py:516
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:523
|
||||
#: build/lib/core/models.py:522 core/models.py:522
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:526
|
||||
#: build/lib/core/models.py:525 core/models.py:525
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:528
|
||||
#: build/lib/core/models.py:527 core/models.py:527
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:531
|
||||
#: build/lib/core/models.py:530 core/models.py:530
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:533
|
||||
#: build/lib/core/models.py:532 core/models.py:532
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:539
|
||||
#: build/lib/core/models.py:538 core/models.py:538
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:558
|
||||
#: build/lib/core/models.py:557 core/models.py:557
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:559
|
||||
#: build/lib/core/models.py:558 core/models.py:558
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:697
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
msgid "Organization/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:698
|
||||
#: build/lib/core/models.py:697 core/models.py:697
|
||||
msgid "Organization/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:703
|
||||
#: build/lib/core/models.py:702 core/models.py:702
|
||||
msgid "This user is already in this organization."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:762
|
||||
msgid "external_id"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:763
|
||||
msgid "Team external UUID for synchronization with external systems"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:788
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
msgid "is visible for all SP"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:790
|
||||
#: build/lib/core/models.py:776 core/models.py:776
|
||||
msgid "Whether this team is visible to all service providers."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:798
|
||||
#: build/lib/core/models.py:784 core/models.py:784
|
||||
msgid "Team"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:799
|
||||
#: build/lib/core/models.py:785 core/models.py:785
|
||||
msgid "Teams"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:850
|
||||
#: build/lib/core/models.py:836 core/models.py:836
|
||||
msgid "Team/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:851
|
||||
#: build/lib/core/models.py:837 core/models.py:837
|
||||
msgid "Team/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:856
|
||||
#: build/lib/core/models.py:842 core/models.py:842
|
||||
msgid "This user is already in this team."
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:942
|
||||
#: build/lib/core/models.py:931 core/models.py:931
|
||||
msgid "url"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:943
|
||||
#: build/lib/core/models.py:932 core/models.py:932
|
||||
msgid "secret"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:957
|
||||
#: build/lib/core/models.py:941 core/models.py:941
|
||||
msgid "Team webhook"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:958
|
||||
#: build/lib/core/models.py:942 core/models.py:942
|
||||
msgid "Team webhooks"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1019
|
||||
#: build/lib/core/models.py:986 core/models.py:986
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Invitation to join La Régie!"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1065
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Team invitation"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1066
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Team invitations"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1079
|
||||
#: build/lib/core/models.py:1060 core/models.py:1060
|
||||
#, python-format
|
||||
msgid "[La Suite] You have been invited to become a %(role)s of a group"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1127
|
||||
#: build/lib/core/models.py:1108 core/models.py:1108
|
||||
msgid "api key"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1132
|
||||
#: build/lib/core/models.py:1113 core/models.py:1113
|
||||
msgid "allowed scopes"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1133
|
||||
#: build/lib/core/models.py:1114 core/models.py:1114
|
||||
msgid "Allowed scopes for this service"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1138
|
||||
#: build/lib/core/models.py:1119 core/models.py:1119
|
||||
msgid "Account service"
|
||||
msgstr ""
|
||||
|
||||
#: core/models.py:1139
|
||||
#: build/lib/core/models.py:1120 core/models.py:1120
|
||||
msgid "Account services"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:17 mailbox_manager/admin.py:17
|
||||
msgid "Import emails from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:35 mailbox_manager/admin.py:35
|
||||
#, python-format
|
||||
msgid "Synchronisation failed for %(domain)s with message: %(err)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:42 mailbox_manager/admin.py:42
|
||||
#, python-format
|
||||
msgid "Synchronisation succeed for %(domain)s. Imported mailboxes: %(mailboxes)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:49 mailbox_manager/admin.py:49
|
||||
#, python-format
|
||||
msgid "Sync require enabled domains. Excluded domains: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:54 mailbox_manager/admin.py:54
|
||||
msgid "Check and update status from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:71 mailbox_manager/admin.py:71
|
||||
#, python-format
|
||||
msgid "- %(domain)s with message: %(err)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:84 mailbox_manager/admin.py:84
|
||||
msgid "Check domains done with success."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:85 mailbox_manager/admin.py:85
|
||||
#, python-format
|
||||
msgid "Domains updated: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:87 mailbox_manager/admin.py:87
|
||||
msgid "No domain updated."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:94 mailbox_manager/admin.py:94
|
||||
msgid "Check domain failed for:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:102 mailbox_manager/admin.py:102
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from check: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:107 mailbox_manager/admin.py:107
|
||||
msgid "Fetch domain expected config from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:121 mailbox_manager/admin.py:121
|
||||
#, python-format
|
||||
msgid "Domain expected config fetched with success for %(domain)s."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:127 mailbox_manager/admin.py:127
|
||||
#, python-format
|
||||
msgid "Failed to fetch domain expected config for %(domain)s."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:133 mailbox_manager/admin.py:133
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from fetch: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:138 mailbox_manager/admin.py:138
|
||||
msgid "Send pending mailboxes to dimail"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:154 mailbox_manager/admin.py:154
|
||||
#, python-format
|
||||
msgid "Failed to send the following mailboxes : %(mailboxes)s."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:160 mailbox_manager/admin.py:160
|
||||
#, python-format
|
||||
msgid "Pending mailboxes successfully sent for %(domain)s."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:166 mailbox_manager/admin.py:166
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from : %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/apps.py:11 mailbox_manager/apps.py:11
|
||||
msgid "Mailbox manager"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:13 mailbox_manager/enums.py:13
|
||||
msgid "Viewer"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:22 build/lib/mailbox_manager/enums.py:32
|
||||
#: mailbox_manager/enums.py:22 mailbox_manager/enums.py:32
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:23 build/lib/mailbox_manager/enums.py:33
|
||||
#: mailbox_manager/enums.py:23 mailbox_manager/enums.py:33
|
||||
msgid "Failed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:24 build/lib/mailbox_manager/enums.py:34
|
||||
#: mailbox_manager/enums.py:24 mailbox_manager/enums.py:34
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:25 mailbox_manager/enums.py:25
|
||||
msgid "Action required"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:32 mailbox_manager/models.py:32
|
||||
msgid "[La Suite] Your domain is ready"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:37 mailbox_manager/models.py:37
|
||||
msgid "[La Suite] Your domain requires action"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:42 mailbox_manager/models.py:42
|
||||
msgid "[La Suite] Your domain has failed"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:68 mailbox_manager/models.py:68
|
||||
msgid "support email"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:72 mailbox_manager/models.py:72
|
||||
msgid "last check details"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:73 mailbox_manager/models.py:73
|
||||
msgid "A JSON object containing the last health check details"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:78 mailbox_manager/models.py:78
|
||||
msgid "expected config"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:79 mailbox_manager/models.py:79
|
||||
msgid "A JSON object containing the expected config"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:84 mailbox_manager/models.py:84
|
||||
msgid "Mail domain"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:85 mailbox_manager/models.py:85
|
||||
msgid "Mail domains"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:199 mailbox_manager/models.py:199
|
||||
msgid "User/mail domain relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:200 mailbox_manager/models.py:200
|
||||
msgid "User/mail domain relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:273 mailbox_manager/models.py:273
|
||||
msgid "local_part"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:287 mailbox_manager/models.py:287
|
||||
msgid "secondary email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:298 mailbox_manager/models.py:298
|
||||
msgid "email"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:304 mailbox_manager/models.py:304
|
||||
msgid "Mailbox"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:305 mailbox_manager/models.py:305
|
||||
msgid "Mailboxes"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:331 mailbox_manager/models.py:331
|
||||
msgid "You can't create or update a mailbox for a disabled domain."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:369 mailbox_manager/models.py:369
|
||||
msgid "Mail domain invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:370 mailbox_manager/models.py:370
|
||||
msgid "Mail domain invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:382 mailbox_manager/models.py:382
|
||||
msgid "[La Suite] You have been invited to join La Régie"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/utils/dimail.py:268
|
||||
#: mailbox_manager/utils/dimail.py:268
|
||||
msgid "Your new mailbox information"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/mailbox_manager/utils/dimail.py:279
|
||||
#: mailbox_manager/utils/dimail.py:279
|
||||
msgid "Your password has been updated"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/people/settings.py:159 people/settings.py:159
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/people/settings.py:160 people/settings.py:160
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/maildomain_action_required.html:195
|
||||
#: core/templates/mail/text/maildomain_action_required.txt:5
|
||||
msgid "Some actions are required on your domain"
|
||||
@@ -638,236 +861,3 @@ msgstr ""
|
||||
msgid "For more information: Visit the website for La Suite numérique [%(link)s] to discover what tools we offer."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:14
|
||||
msgid "Import emails from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:32 mailbox_manager/admin.py:71
|
||||
#, python-format
|
||||
msgid "Synchronisation failed for %(domain)s with message: %(err)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:39
|
||||
#, python-format
|
||||
msgid "Synchronisation succeed for %(domain)s. Imported mailboxes: %(mailboxes)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:46 mailbox_manager/admin.py:91
|
||||
#, python-format
|
||||
msgid "Sync require enabled domains. Excluded domains: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:51
|
||||
msgid "Import aliases from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:78
|
||||
#, python-format
|
||||
msgid "Synchronisation succeed for %(domain)s.Imported %(count_imported)s aliases: %(aliases)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:96
|
||||
msgid "Check and update status from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:113
|
||||
#, python-format
|
||||
msgid "- %(domain)s with message: %(err)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:126
|
||||
msgid "Check domains done with success."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:127
|
||||
#, python-format
|
||||
msgid "Domains updated: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:129
|
||||
msgid "No domain updated."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:136
|
||||
msgid "Check domain failed for:"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:144
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from check: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:149
|
||||
msgid "Fetch domain expected config from dimail"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:163
|
||||
#, python-format
|
||||
msgid "Domain expected config fetched with success for %(domain)s."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:169
|
||||
#, python-format
|
||||
msgid "Failed to fetch domain expected config for %(domain)s."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:175
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from fetch: %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:180
|
||||
msgid "Send pending mailboxes to dimail"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:196
|
||||
#, python-format
|
||||
msgid "Failed to send the following mailboxes : %(mailboxes)s."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:202
|
||||
#, python-format
|
||||
msgid "Pending mailboxes successfully sent for %(domain)s."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/admin.py:208
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from : %(domains)s"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/apps.py:11
|
||||
msgid "Mailbox manager"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:13
|
||||
msgid "Viewer"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:22 mailbox_manager/enums.py:32
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:23 mailbox_manager/enums.py:33
|
||||
msgid "Failed"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:24 mailbox_manager/enums.py:34
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/enums.py:25
|
||||
msgid "Action required"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:35
|
||||
msgid "[La Suite] Your domain is ready"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:40
|
||||
msgid "[La Suite] Your domain requires action"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:45
|
||||
msgid "[La Suite] Your domain has failed"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:71
|
||||
msgid "support email"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:75
|
||||
msgid "last check details"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:76
|
||||
msgid "A JSON object containing the last health check details"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:81
|
||||
msgid "expected config"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:82
|
||||
msgid "A JSON object containing the expected config"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:87
|
||||
msgid "Mail domain"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:88
|
||||
msgid "Mail domains"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:204
|
||||
msgid "User/mail domain relation"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:205
|
||||
msgid "User/mail domain relations"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:278
|
||||
msgid "local_part"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:292
|
||||
msgid "secondary email address"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:303
|
||||
msgid "email"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:309
|
||||
msgid "Mailbox"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:310
|
||||
msgid "Mailboxes"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:352
|
||||
msgid "You can't create or update a mailbox for a disabled domain."
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:411
|
||||
msgid "Mail domain invitation"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:412
|
||||
msgid "Mail domain invitations"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:441
|
||||
msgid "[La Suite] You have been invited to join La Régie"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:485
|
||||
msgid "destination address"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:503
|
||||
msgid "Alias"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/models.py:504
|
||||
msgid "Aliases"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/utils/dimail.py:296
|
||||
msgid "Your new mailbox information"
|
||||
msgstr ""
|
||||
|
||||
#: mailbox_manager/utils/dimail.py:307
|
||||
msgid "Your password has been updated"
|
||||
msgstr ""
|
||||
|
||||
#: people/settings.py:159
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#: people/settings.py:160
|
||||
msgid "French"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-people\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-13 15:21+0000\n"
|
||||
"PO-Revision-Date: 2026-03-24 08:54\n"
|
||||
"POT-Creation-Date: 2025-06-10 16:13+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 09:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -17,336 +17,559 @@ msgstr ""
|
||||
"X-Crowdin-File: backend.pot\n"
|
||||
"X-Crowdin-File-ID: 2\n"
|
||||
|
||||
#: core/admin.py:62
|
||||
#: build/lib/core/admin.py:62 core/admin.py:62
|
||||
msgid "Personal info"
|
||||
msgstr "Informations personnelles"
|
||||
|
||||
#: core/admin.py:73
|
||||
#: build/lib/core/admin.py:73 core/admin.py:73
|
||||
msgid "Permissions"
|
||||
msgstr "Permissions"
|
||||
|
||||
#: core/admin.py:85
|
||||
#: build/lib/core/admin.py:85 core/admin.py:85
|
||||
msgid "Important dates"
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: core/admin.py:124
|
||||
#: build/lib/core/admin.py:124 core/admin.py:124
|
||||
msgid "User"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#: core/admin.py:226
|
||||
#: build/lib/core/admin.py:226 core/admin.py:226
|
||||
msgid "Run post creation plugins"
|
||||
msgstr "Exécuter les plugins de post-création"
|
||||
|
||||
#: core/admin.py:234
|
||||
#: build/lib/core/admin.py:234 core/admin.py:234
|
||||
msgid "Post creation plugins have been run for the selected organizations."
|
||||
msgstr "Les plugins de post-création ont été exécutés pour les organisations sélectionnées."
|
||||
|
||||
#: core/apps.py:65
|
||||
#: build/lib/core/apps.py:65 core/apps.py:65
|
||||
msgid "People core application"
|
||||
msgstr "Application cœur de People"
|
||||
|
||||
#: core/authentication/backends.py:102
|
||||
#: build/lib/core/authentication/backends.py:104
|
||||
#: core/authentication/backends.py:104
|
||||
msgid "Claims contained no recognizable user identification"
|
||||
msgstr "Les claims ne contiennent aucune identification reconnaissable pour l'organisation"
|
||||
|
||||
#: core/authentication/backends.py:122
|
||||
#: build/lib/core/authentication/backends.py:124
|
||||
#: core/authentication/backends.py:124
|
||||
msgid "Claims contained no recognizable organization identification"
|
||||
msgstr "Les claims ne contiennent aucune identification reconnaissable pour l'organisation"
|
||||
|
||||
#: core/authentication/backends.py:170 core/authentication/backends.py:172
|
||||
#: build/lib/core/authentication/backends.py:181
|
||||
#: build/lib/core/authentication/backends.py:183
|
||||
#: core/authentication/backends.py:181 core/authentication/backends.py:183
|
||||
msgid "Invalid authorization header."
|
||||
msgstr "En-tête d'autorisation invalide."
|
||||
|
||||
#: core/authentication/backends.py:177
|
||||
#: build/lib/core/authentication/backends.py:188
|
||||
#: core/authentication/backends.py:188
|
||||
msgid "Invalid api key."
|
||||
msgstr "Clé API invalide."
|
||||
|
||||
#: core/enums.py:24
|
||||
#: build/lib/core/enums.py:24 core/enums.py:24
|
||||
msgid "Failure"
|
||||
msgstr "En échec"
|
||||
|
||||
#: core/enums.py:25 mailbox_manager/enums.py:21 mailbox_manager/enums.py:31
|
||||
#: build/lib/core/enums.py:25 build/lib/mailbox_manager/enums.py:21
|
||||
#: build/lib/mailbox_manager/enums.py:31 core/enums.py:25
|
||||
#: mailbox_manager/enums.py:21 mailbox_manager/enums.py:31
|
||||
msgid "Pending"
|
||||
msgstr "En attente"
|
||||
|
||||
#: core/enums.py:26
|
||||
#: build/lib/core/enums.py:26 core/enums.py:26
|
||||
msgid "Success"
|
||||
msgstr "Réussi"
|
||||
|
||||
#: core/exceptions.py:16
|
||||
msgid "Email already known. Invitation not sent but access created instead."
|
||||
msgstr "Adresse email déjà connue. Aucune invitation envoyée mais accès créé à la place."
|
||||
|
||||
#: core/models.py:77
|
||||
#: build/lib/core/models.py:76 core/models.py:76
|
||||
msgid "Member"
|
||||
msgstr "Membre"
|
||||
|
||||
#: core/models.py:78 core/models.py:90 mailbox_manager/enums.py:14
|
||||
#: build/lib/core/models.py:77 build/lib/core/models.py:89
|
||||
#: build/lib/mailbox_manager/enums.py:14 core/models.py:77 core/models.py:89
|
||||
#: mailbox_manager/enums.py:14
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: core/models.py:79 mailbox_manager/enums.py:15
|
||||
#: build/lib/core/models.py:78 build/lib/mailbox_manager/enums.py:15
|
||||
#: core/models.py:78 mailbox_manager/enums.py:15
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: core/models.py:102
|
||||
#: build/lib/core/models.py:101 core/models.py:101
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:103
|
||||
#: build/lib/core/models.py:102 core/models.py:102
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clé primaire pour l'enregistrement en tant que UUID"
|
||||
|
||||
#: core/models.py:109
|
||||
#: build/lib/core/models.py:108 core/models.py:108
|
||||
msgid "created at"
|
||||
msgstr "créé le"
|
||||
|
||||
#: core/models.py:110
|
||||
#: build/lib/core/models.py:109 core/models.py:109
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date et heure de création de l'enregistrement"
|
||||
|
||||
#: core/models.py:115
|
||||
#: build/lib/core/models.py:114 core/models.py:114
|
||||
msgid "updated at"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:116
|
||||
#: build/lib/core/models.py:115 core/models.py:115
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
|
||||
|
||||
#: core/models.py:155
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:156
|
||||
#: build/lib/core/models.py:155 core/models.py:155
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:159
|
||||
#: build/lib/core/models.py:158 core/models.py:158
|
||||
msgid "notes"
|
||||
msgstr "notes"
|
||||
|
||||
#: core/models.py:161
|
||||
#: build/lib/core/models.py:160 core/models.py:160
|
||||
msgid "contact information"
|
||||
msgstr "informations de contact"
|
||||
|
||||
#: core/models.py:162
|
||||
#: build/lib/core/models.py:161 core/models.py:161
|
||||
msgid "A JSON object containing the contact information"
|
||||
msgstr "Un objet JSON contenant les informations de contact"
|
||||
|
||||
#: core/models.py:176
|
||||
#: build/lib/core/models.py:175 core/models.py:175
|
||||
msgid "contact"
|
||||
msgstr "contact"
|
||||
|
||||
#: core/models.py:177
|
||||
#: build/lib/core/models.py:176 core/models.py:176
|
||||
msgid "contacts"
|
||||
msgstr "contacts"
|
||||
|
||||
#: core/models.py:251 core/models.py:365 core/models.py:511 core/models.py:1125
|
||||
#: mailbox_manager/models.py:56
|
||||
#: build/lib/core/models.py:250 build/lib/core/models.py:364
|
||||
#: build/lib/core/models.py:510 build/lib/core/models.py:1106
|
||||
#: build/lib/mailbox_manager/models.py:53 core/models.py:250 core/models.py:364
|
||||
#: core/models.py:510 core/models.py:1106 mailbox_manager/models.py:53
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: core/models.py:253
|
||||
#: build/lib/core/models.py:252 core/models.py:252
|
||||
msgid "audience id"
|
||||
msgstr "ID d'audience"
|
||||
|
||||
#: core/models.py:258
|
||||
#: build/lib/core/models.py:257 core/models.py:257
|
||||
msgid "service provider"
|
||||
msgstr "fournisseur de services"
|
||||
|
||||
#: core/models.py:259
|
||||
#: build/lib/core/models.py:258 core/models.py:258
|
||||
msgid "service providers"
|
||||
msgstr "fournisseurs de services"
|
||||
|
||||
#: core/models.py:373
|
||||
#: build/lib/core/models.py:372 core/models.py:372
|
||||
msgid "registration ID list"
|
||||
msgstr "liste d'identifiants d'inscription"
|
||||
|
||||
#: core/models.py:380
|
||||
#: build/lib/core/models.py:379 core/models.py:379
|
||||
msgid "domain list"
|
||||
msgstr "liste des domaines"
|
||||
|
||||
#: core/models.py:387
|
||||
#: build/lib/core/models.py:386 core/models.py:386
|
||||
msgid "metadata"
|
||||
msgstr "métadonnées"
|
||||
|
||||
#: core/models.py:388
|
||||
#: build/lib/core/models.py:387 core/models.py:387
|
||||
msgid "A JSON object containing the organization metadata"
|
||||
msgstr "Un objet JSON contenant les métadonnées de l'organisation"
|
||||
|
||||
#: core/models.py:398 core/models.py:536
|
||||
#: build/lib/core/models.py:397 build/lib/core/models.py:535 core/models.py:397
|
||||
#: core/models.py:535
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: core/models.py:404
|
||||
#: build/lib/core/models.py:403 core/models.py:403
|
||||
msgid "organization"
|
||||
msgstr "organisation"
|
||||
|
||||
#: core/models.py:405
|
||||
#: build/lib/core/models.py:404 core/models.py:404
|
||||
msgid "organizations"
|
||||
msgstr "organisations"
|
||||
|
||||
#: core/models.py:412
|
||||
#: build/lib/core/models.py:411 core/models.py:411
|
||||
msgid "An organization must have at least a registration ID or a domain."
|
||||
msgstr "Une organisation doit avoir au moins un ID d'enregistrement ou un domaine."
|
||||
|
||||
#: core/models.py:496
|
||||
#: build/lib/core/models.py:495 core/models.py:495
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters."
|
||||
msgstr "Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des chiffres et des caractères @/./+/-/_."
|
||||
|
||||
#: core/models.py:502
|
||||
#: build/lib/core/models.py:501 core/models.py:501
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:504
|
||||
#: build/lib/core/models.py:503 core/models.py:503
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
msgstr "Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_ seulement."
|
||||
|
||||
#: core/models.py:510 core/models.py:974
|
||||
#: build/lib/core/models.py:509 build/lib/core/models.py:958 core/models.py:509
|
||||
#: core/models.py:958
|
||||
msgid "email address"
|
||||
msgstr "adresse email"
|
||||
|
||||
#: core/models.py:516
|
||||
#: build/lib/core/models.py:515 core/models.py:515
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:517
|
||||
#: build/lib/core/models.py:516 core/models.py:516
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
|
||||
|
||||
#: core/models.py:523
|
||||
#: build/lib/core/models.py:522 core/models.py:522
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:526
|
||||
#: build/lib/core/models.py:525 core/models.py:525
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:528
|
||||
#: build/lib/core/models.py:527 core/models.py:527
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:531
|
||||
#: build/lib/core/models.py:530 core/models.py:530
|
||||
msgid "staff status"
|
||||
msgstr "statut d'équipe"
|
||||
|
||||
#: core/models.py:533
|
||||
#: build/lib/core/models.py:532 core/models.py:532
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: core/models.py:539
|
||||
#: build/lib/core/models.py:538 core/models.py:538
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
|
||||
|
||||
#: core/models.py:558
|
||||
#: build/lib/core/models.py:557 core/models.py:557
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:559
|
||||
#: build/lib/core/models.py:558 core/models.py:558
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:697
|
||||
#: build/lib/core/models.py:696 core/models.py:696
|
||||
msgid "Organization/user relation"
|
||||
msgstr "Relation organisation/utilisateur"
|
||||
|
||||
#: core/models.py:698
|
||||
#: build/lib/core/models.py:697 core/models.py:697
|
||||
msgid "Organization/user relations"
|
||||
msgstr "Relations organisation/utilisateur"
|
||||
|
||||
#: core/models.py:703
|
||||
#: build/lib/core/models.py:702 core/models.py:702
|
||||
msgid "This user is already in this organization."
|
||||
msgstr "Cet utilisateur est déjà dans cette organisation."
|
||||
|
||||
#: core/models.py:762
|
||||
msgid "external_id"
|
||||
msgstr "ID externe"
|
||||
|
||||
#: core/models.py:763
|
||||
msgid "Team external UUID for synchronization with external systems"
|
||||
msgstr "UUID externe de l'équipe pour la synchronisation avec des systèmes externes"
|
||||
|
||||
#: core/models.py:788
|
||||
#: build/lib/core/models.py:774 core/models.py:774
|
||||
msgid "is visible for all SP"
|
||||
msgstr "est visible pour tous les fournisseurs de service"
|
||||
|
||||
#: core/models.py:790
|
||||
#: build/lib/core/models.py:776 core/models.py:776
|
||||
msgid "Whether this team is visible to all service providers."
|
||||
msgstr "Si cette équipe est visible pour tous les fournisseurs de services."
|
||||
|
||||
#: core/models.py:798
|
||||
#: build/lib/core/models.py:784 core/models.py:784
|
||||
msgid "Team"
|
||||
msgstr "Équipe"
|
||||
|
||||
#: core/models.py:799
|
||||
#: build/lib/core/models.py:785 core/models.py:785
|
||||
msgid "Teams"
|
||||
msgstr "Équipes"
|
||||
|
||||
#: core/models.py:850
|
||||
#: build/lib/core/models.py:836 core/models.py:836
|
||||
msgid "Team/user relation"
|
||||
msgstr "Relation équipe/utilisateur"
|
||||
|
||||
#: core/models.py:851
|
||||
#: build/lib/core/models.py:837 core/models.py:837
|
||||
msgid "Team/user relations"
|
||||
msgstr "Relations équipe/utilisateur"
|
||||
|
||||
#: core/models.py:856
|
||||
#: build/lib/core/models.py:842 core/models.py:842
|
||||
msgid "This user is already in this team."
|
||||
msgstr "Cet utilisateur est déjà dans cette équipe."
|
||||
|
||||
#: core/models.py:942
|
||||
#: build/lib/core/models.py:931 core/models.py:931
|
||||
msgid "url"
|
||||
msgstr "url"
|
||||
|
||||
#: core/models.py:943
|
||||
#: build/lib/core/models.py:932 core/models.py:932
|
||||
msgid "secret"
|
||||
msgstr "secret"
|
||||
|
||||
#: core/models.py:957
|
||||
#: build/lib/core/models.py:941 core/models.py:941
|
||||
msgid "Team webhook"
|
||||
msgstr "Webhook d'équipe"
|
||||
|
||||
#: core/models.py:958
|
||||
#: build/lib/core/models.py:942 core/models.py:942
|
||||
msgid "Team webhooks"
|
||||
msgstr "Webhooks d'équipe"
|
||||
|
||||
#: core/models.py:1019
|
||||
#: build/lib/core/models.py:986 core/models.py:986
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Cette adresse email est déjà associée à un utilisateur enregistré."
|
||||
|
||||
#: build/lib/core/models.py:1000 core/models.py:1000
|
||||
msgid "Invitation to join La Régie!"
|
||||
msgstr "Invitation à rejoindre La Régie !"
|
||||
|
||||
#: core/models.py:1065
|
||||
#: build/lib/core/models.py:1046 core/models.py:1046
|
||||
msgid "Team invitation"
|
||||
msgstr "Invitation d'équipe"
|
||||
|
||||
#: core/models.py:1066
|
||||
#: build/lib/core/models.py:1047 core/models.py:1047
|
||||
msgid "Team invitations"
|
||||
msgstr "Invitations d'équipe"
|
||||
|
||||
#: core/models.py:1079
|
||||
#: build/lib/core/models.py:1060 core/models.py:1060
|
||||
#, python-format
|
||||
msgid "[La Suite] You have been invited to become a %(role)s of a group"
|
||||
msgstr "[La Suite] Vous avez été invité(e) à être %(role)s d'un groupe"
|
||||
|
||||
#: core/models.py:1127
|
||||
#: build/lib/core/models.py:1108 core/models.py:1108
|
||||
msgid "api key"
|
||||
msgstr "Clé d'API"
|
||||
|
||||
#: core/models.py:1132
|
||||
#: build/lib/core/models.py:1113 core/models.py:1113
|
||||
msgid "allowed scopes"
|
||||
msgstr "périmètres autorisés"
|
||||
|
||||
#: core/models.py:1133
|
||||
#: build/lib/core/models.py:1114 core/models.py:1114
|
||||
msgid "Allowed scopes for this service"
|
||||
msgstr "Périmètres d'application autorisés pour ce service"
|
||||
|
||||
#: core/models.py:1138
|
||||
#: build/lib/core/models.py:1119 core/models.py:1119
|
||||
msgid "Account service"
|
||||
msgstr "Compte de service"
|
||||
|
||||
#: core/models.py:1139
|
||||
#: build/lib/core/models.py:1120 core/models.py:1120
|
||||
msgid "Account services"
|
||||
msgstr "Comptes de service"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:17 mailbox_manager/admin.py:17
|
||||
msgid "Import emails from dimail"
|
||||
msgstr "Importer les emails depuis dimail"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:35 mailbox_manager/admin.py:35
|
||||
#, python-format
|
||||
msgid "Synchronisation failed for %(domain)s with message: %(err)s"
|
||||
msgstr "La synchronisation a échoué pour %(domain)s avec le message : %(err)s"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:42 mailbox_manager/admin.py:42
|
||||
#, python-format
|
||||
msgid "Synchronisation succeed for %(domain)s. Imported mailboxes: %(mailboxes)s"
|
||||
msgstr "La synchronisation a réussi pour %(domain)s. Importation des boîtes mails : %(mailboxes)s"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:49 mailbox_manager/admin.py:49
|
||||
#, python-format
|
||||
msgid "Sync require enabled domains. Excluded domains: %(domains)s"
|
||||
msgstr "La synchro nécessite des domaines activés. Les domaines exclus sont : %(domains)s"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:54 mailbox_manager/admin.py:54
|
||||
msgid "Check and update status from dimail"
|
||||
msgstr "Vérifier et mettre à jour le statut à partir de dimail"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:71 mailbox_manager/admin.py:71
|
||||
#, python-format
|
||||
msgid "- %(domain)s with message: %(err)s"
|
||||
msgstr "- %(domain)s avec le message : %(err)s"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:84 mailbox_manager/admin.py:84
|
||||
msgid "Check domains done with success."
|
||||
msgstr "Vérification des domaines effectuée avec succès."
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:85 mailbox_manager/admin.py:85
|
||||
#, python-format
|
||||
msgid "Domains updated: %(domains)s"
|
||||
msgstr "Domaines mis à jour : %(domains)s"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:87 mailbox_manager/admin.py:87
|
||||
msgid "No domain updated."
|
||||
msgstr "Aucun domaine mis à jour."
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:94 mailbox_manager/admin.py:94
|
||||
msgid "Check domain failed for:"
|
||||
msgstr "La vérification du domaine a échoué pour :"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:102 mailbox_manager/admin.py:102
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from check: %(domains)s"
|
||||
msgstr "Les domaines désactivés sont exclus de la vérification : %(domains)s"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:107 mailbox_manager/admin.py:107
|
||||
msgid "Fetch domain expected config from dimail"
|
||||
msgstr "Récupérer la configuration attendue du domaine depuis dimail"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:121 mailbox_manager/admin.py:121
|
||||
#, python-format
|
||||
msgid "Domain expected config fetched with success for %(domain)s."
|
||||
msgstr "La configuration du domaine attendue a été récupérée avec succès pour %(domain)s."
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:127 mailbox_manager/admin.py:127
|
||||
#, python-format
|
||||
msgid "Failed to fetch domain expected config for %(domain)s."
|
||||
msgstr "Impossible de récupérer la configuration attendue pour %(domain)s."
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:133 mailbox_manager/admin.py:133
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from fetch: %(domains)s"
|
||||
msgstr "Les domaines désactivés sont exclus de la récupération : %(domains)s"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:138 mailbox_manager/admin.py:138
|
||||
msgid "Send pending mailboxes to dimail"
|
||||
msgstr "Envoyer les adresses mail en attente à dimail"
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:154 mailbox_manager/admin.py:154
|
||||
#, python-format
|
||||
msgid "Failed to send the following mailboxes : %(mailboxes)s."
|
||||
msgstr "Échec de l'envoi des adresses mail suivantes : %(mailboxes)s."
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:160 mailbox_manager/admin.py:160
|
||||
#, python-format
|
||||
msgid "Pending mailboxes successfully sent for %(domain)s."
|
||||
msgstr "Succès de l'envoi des adresses en attente pour le domaine %(domain)s."
|
||||
|
||||
#: build/lib/mailbox_manager/admin.py:166 mailbox_manager/admin.py:166
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from : %(domains)s"
|
||||
msgstr "Les domaines désactivés ont été exclu : %(domains)s"
|
||||
|
||||
#: build/lib/mailbox_manager/apps.py:11 mailbox_manager/apps.py:11
|
||||
msgid "Mailbox manager"
|
||||
msgstr "Gestion des boîtes mails"
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:13 mailbox_manager/enums.py:13
|
||||
msgid "Viewer"
|
||||
msgstr "Lecteur"
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:22 build/lib/mailbox_manager/enums.py:32
|
||||
#: mailbox_manager/enums.py:22 mailbox_manager/enums.py:32
|
||||
msgid "Enabled"
|
||||
msgstr "Actif"
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:23 build/lib/mailbox_manager/enums.py:33
|
||||
#: mailbox_manager/enums.py:23 mailbox_manager/enums.py:33
|
||||
msgid "Failed"
|
||||
msgstr "En erreur"
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:24 build/lib/mailbox_manager/enums.py:34
|
||||
#: mailbox_manager/enums.py:24 mailbox_manager/enums.py:34
|
||||
msgid "Disabled"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#: build/lib/mailbox_manager/enums.py:25 mailbox_manager/enums.py:25
|
||||
msgid "Action required"
|
||||
msgstr "Action requise"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:32 mailbox_manager/models.py:32
|
||||
msgid "[La Suite] Your domain is ready"
|
||||
msgstr "[La Suite] Votre domaine est prêt"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:37 mailbox_manager/models.py:37
|
||||
msgid "[La Suite] Your domain requires action"
|
||||
msgstr "[La Suite] Des actions sont requises sur votre domaine"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:42 mailbox_manager/models.py:42
|
||||
msgid "[La Suite] Your domain has failed"
|
||||
msgstr "[La Suite] Votre domaine est en erreur"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:68 mailbox_manager/models.py:68
|
||||
msgid "support email"
|
||||
msgstr "adresse email du support"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:72 mailbox_manager/models.py:72
|
||||
msgid "last check details"
|
||||
msgstr "détails de la dernière vérification"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:73 mailbox_manager/models.py:73
|
||||
msgid "A JSON object containing the last health check details"
|
||||
msgstr "Un objet JSON contenant les derniers détails du bilan de santé"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:78 mailbox_manager/models.py:78
|
||||
msgid "expected config"
|
||||
msgstr "configuration attendue"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:79 mailbox_manager/models.py:79
|
||||
msgid "A JSON object containing the expected config"
|
||||
msgstr "Un objet JSON contenant la configuration attendue"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:84 mailbox_manager/models.py:84
|
||||
msgid "Mail domain"
|
||||
msgstr "Domaine de messagerie"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:85 mailbox_manager/models.py:85
|
||||
msgid "Mail domains"
|
||||
msgstr "Domaines de messagerie"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:199 mailbox_manager/models.py:199
|
||||
msgid "User/mail domain relation"
|
||||
msgstr "Relation utilisateur/domaine de messagerie"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:200 mailbox_manager/models.py:200
|
||||
msgid "User/mail domain relations"
|
||||
msgstr "Relations utilisateur/domaine de messagerie"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:273 mailbox_manager/models.py:273
|
||||
msgid "local_part"
|
||||
msgstr "local_part"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:287 mailbox_manager/models.py:287
|
||||
msgid "secondary email address"
|
||||
msgstr "adresse email secondaire"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:298 mailbox_manager/models.py:298
|
||||
msgid "email"
|
||||
msgstr "email"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:304 mailbox_manager/models.py:304
|
||||
msgid "Mailbox"
|
||||
msgstr "Boîte mail"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:305 mailbox_manager/models.py:305
|
||||
msgid "Mailboxes"
|
||||
msgstr "Boîtes mail"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:331 mailbox_manager/models.py:331
|
||||
msgid "You can't create or update a mailbox for a disabled domain."
|
||||
msgstr "Vous ne pouvez pas créer ou mettre à jour une boîte mail pour un domaine désactivé."
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:369 mailbox_manager/models.py:369
|
||||
msgid "Mail domain invitation"
|
||||
msgstr "Invitation au domaine de messagerie"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:370 mailbox_manager/models.py:370
|
||||
msgid "Mail domain invitations"
|
||||
msgstr "Invitations au domaine de messagerie"
|
||||
|
||||
#: build/lib/mailbox_manager/models.py:382 mailbox_manager/models.py:382
|
||||
msgid "[La Suite] You have been invited to join La Régie"
|
||||
msgstr "[La Suite] Vous avez été invité(e) à rejoindre la Régie"
|
||||
|
||||
#: build/lib/mailbox_manager/utils/dimail.py:268
|
||||
#: mailbox_manager/utils/dimail.py:268
|
||||
msgid "Your new mailbox information"
|
||||
msgstr "Informations sur votre nouvelle boîte mail"
|
||||
|
||||
#: build/lib/mailbox_manager/utils/dimail.py:279
|
||||
#: mailbox_manager/utils/dimail.py:279
|
||||
msgid "Your password has been updated"
|
||||
msgstr "Votre mot de passe a été mis à jour"
|
||||
|
||||
#: build/lib/people/settings.py:159 people/settings.py:159
|
||||
msgid "English"
|
||||
msgstr "Anglais"
|
||||
|
||||
#: build/lib/people/settings.py:160 people/settings.py:160
|
||||
msgid "French"
|
||||
msgstr "Français"
|
||||
|
||||
#: core/templates/mail/html/maildomain_action_required.html:195
|
||||
#: core/templates/mail/text/maildomain_action_required.txt:5
|
||||
msgid "Some actions are required on your domain"
|
||||
@@ -429,7 +652,7 @@ msgstr "Hourra !"
|
||||
#: core/templates/mail/html/maildomain_enabled.html:206
|
||||
#, python-format
|
||||
msgid "Your domain <b>%(name)s</b> can be used now."
|
||||
msgstr "Votre domaine <b>%(name)s</b> est prêt à être utilisé."
|
||||
msgstr "Votre domaine <b>%(name)s</b> est prêt à être exploité."
|
||||
|
||||
#: core/templates/mail/html/maildomain_enabled.html:211
|
||||
#: core/templates/mail/html/maildomain_invitation.html:211
|
||||
@@ -638,236 +861,3 @@ msgstr "Le domaine %(name)s a rencontré une erreur. Tant que cette erreur persi
|
||||
msgid "For more information: Visit the website for La Suite numérique [%(link)s] to discover what tools we offer."
|
||||
msgstr "Pour en savoir plus : Visitez le site de la Suite numérique [%(link)s] pour découvrir l’ensemble des outils proposés."
|
||||
|
||||
#: mailbox_manager/admin.py:14
|
||||
msgid "Import emails from dimail"
|
||||
msgstr "Importer les emails depuis dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:32 mailbox_manager/admin.py:71
|
||||
#, python-format
|
||||
msgid "Synchronisation failed for %(domain)s with message: %(err)s"
|
||||
msgstr "La synchronisation a échoué pour %(domain)s avec le message : %(err)s"
|
||||
|
||||
#: mailbox_manager/admin.py:39
|
||||
#, python-format
|
||||
msgid "Synchronisation succeed for %(domain)s. Imported mailboxes: %(mailboxes)s"
|
||||
msgstr "La synchronisation a réussi pour %(domain)s. Importation des boîtes mails : %(mailboxes)s"
|
||||
|
||||
#: mailbox_manager/admin.py:46 mailbox_manager/admin.py:91
|
||||
#, python-format
|
||||
msgid "Sync require enabled domains. Excluded domains: %(domains)s"
|
||||
msgstr "La synchro nécessite des domaines activés. Les domaines exclus sont : %(domains)s"
|
||||
|
||||
#: mailbox_manager/admin.py:51
|
||||
msgid "Import aliases from dimail"
|
||||
msgstr "Importer des alias depuis dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:78
|
||||
#, python-format
|
||||
msgid "Synchronisation succeed for %(domain)s.Imported %(count_imported)s aliases: %(aliases)s"
|
||||
msgstr "Synchronisation réussie pour %(domain)s. %(count_imported)s alias importés : %(aliases)s"
|
||||
|
||||
#: mailbox_manager/admin.py:96
|
||||
msgid "Check and update status from dimail"
|
||||
msgstr "Vérifier et mettre à jour le statut à partir de dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:113
|
||||
#, python-format
|
||||
msgid "- %(domain)s with message: %(err)s"
|
||||
msgstr "- %(domain)s avec le message : %(err)s"
|
||||
|
||||
#: mailbox_manager/admin.py:126
|
||||
msgid "Check domains done with success."
|
||||
msgstr "Vérification des domaines effectuée avec succès."
|
||||
|
||||
#: mailbox_manager/admin.py:127
|
||||
#, python-format
|
||||
msgid "Domains updated: %(domains)s"
|
||||
msgstr "Domaines mis à jour : %(domains)s"
|
||||
|
||||
#: mailbox_manager/admin.py:129
|
||||
msgid "No domain updated."
|
||||
msgstr "Aucun domaine mis à jour."
|
||||
|
||||
#: mailbox_manager/admin.py:136
|
||||
msgid "Check domain failed for:"
|
||||
msgstr "La vérification du domaine a échoué pour :"
|
||||
|
||||
#: mailbox_manager/admin.py:144
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from check: %(domains)s"
|
||||
msgstr "Les domaines désactivés sont exclus de la vérification : %(domains)s"
|
||||
|
||||
#: mailbox_manager/admin.py:149
|
||||
msgid "Fetch domain expected config from dimail"
|
||||
msgstr "Récupérer la configuration attendue du domaine depuis dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:163
|
||||
#, python-format
|
||||
msgid "Domain expected config fetched with success for %(domain)s."
|
||||
msgstr "La configuration du domaine attendue a été récupérée avec succès pour %(domain)s."
|
||||
|
||||
#: mailbox_manager/admin.py:169
|
||||
#, python-format
|
||||
msgid "Failed to fetch domain expected config for %(domain)s."
|
||||
msgstr "Impossible de récupérer la configuration attendue pour %(domain)s."
|
||||
|
||||
#: mailbox_manager/admin.py:175
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from fetch: %(domains)s"
|
||||
msgstr "Les domaines désactivés sont exclus de la récupération : %(domains)s"
|
||||
|
||||
#: mailbox_manager/admin.py:180
|
||||
msgid "Send pending mailboxes to dimail"
|
||||
msgstr "Envoyer les adresses mail en attente à dimail"
|
||||
|
||||
#: mailbox_manager/admin.py:196
|
||||
#, python-format
|
||||
msgid "Failed to send the following mailboxes : %(mailboxes)s."
|
||||
msgstr "Échec de l'envoi des adresses mail suivantes : %(mailboxes)s."
|
||||
|
||||
#: mailbox_manager/admin.py:202
|
||||
#, python-format
|
||||
msgid "Pending mailboxes successfully sent for %(domain)s."
|
||||
msgstr "Succès de l'envoi des adresses en attente pour le domaine %(domain)s."
|
||||
|
||||
#: mailbox_manager/admin.py:208
|
||||
#, python-format
|
||||
msgid "Domains disabled are excluded from : %(domains)s"
|
||||
msgstr "Les domaines désactivés ont été exclu : %(domains)s"
|
||||
|
||||
#: mailbox_manager/apps.py:11
|
||||
msgid "Mailbox manager"
|
||||
msgstr "Gestion des boîtes mails"
|
||||
|
||||
#: mailbox_manager/enums.py:13
|
||||
msgid "Viewer"
|
||||
msgstr "Lecteur"
|
||||
|
||||
#: mailbox_manager/enums.py:22 mailbox_manager/enums.py:32
|
||||
msgid "Enabled"
|
||||
msgstr "Actif"
|
||||
|
||||
#: mailbox_manager/enums.py:23 mailbox_manager/enums.py:33
|
||||
msgid "Failed"
|
||||
msgstr "En erreur"
|
||||
|
||||
#: mailbox_manager/enums.py:24 mailbox_manager/enums.py:34
|
||||
msgid "Disabled"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#: mailbox_manager/enums.py:25
|
||||
msgid "Action required"
|
||||
msgstr "Action requise"
|
||||
|
||||
#: mailbox_manager/models.py:35
|
||||
msgid "[La Suite] Your domain is ready"
|
||||
msgstr "[La Suite] Votre domaine est prêt"
|
||||
|
||||
#: mailbox_manager/models.py:40
|
||||
msgid "[La Suite] Your domain requires action"
|
||||
msgstr "[La Suite] Des actions sont requises sur votre domaine"
|
||||
|
||||
#: mailbox_manager/models.py:45
|
||||
msgid "[La Suite] Your domain has failed"
|
||||
msgstr "[La Suite] Votre domaine est en erreur"
|
||||
|
||||
#: mailbox_manager/models.py:71
|
||||
msgid "support email"
|
||||
msgstr "adresse email du support"
|
||||
|
||||
#: mailbox_manager/models.py:75
|
||||
msgid "last check details"
|
||||
msgstr "détails de la dernière vérification"
|
||||
|
||||
#: mailbox_manager/models.py:76
|
||||
msgid "A JSON object containing the last health check details"
|
||||
msgstr "Un objet JSON contenant les derniers détails du bilan de santé"
|
||||
|
||||
#: mailbox_manager/models.py:81
|
||||
msgid "expected config"
|
||||
msgstr "configuration attendue"
|
||||
|
||||
#: mailbox_manager/models.py:82
|
||||
msgid "A JSON object containing the expected config"
|
||||
msgstr "Un objet JSON contenant la configuration attendue"
|
||||
|
||||
#: mailbox_manager/models.py:87
|
||||
msgid "Mail domain"
|
||||
msgstr "Domaine de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:88
|
||||
msgid "Mail domains"
|
||||
msgstr "Domaines de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:204
|
||||
msgid "User/mail domain relation"
|
||||
msgstr "Relation utilisateur/domaine de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:205
|
||||
msgid "User/mail domain relations"
|
||||
msgstr "Relations utilisateur/domaine de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:278
|
||||
msgid "local_part"
|
||||
msgstr "local_part"
|
||||
|
||||
#: mailbox_manager/models.py:292
|
||||
msgid "secondary email address"
|
||||
msgstr "adresse email secondaire"
|
||||
|
||||
#: mailbox_manager/models.py:303
|
||||
msgid "email"
|
||||
msgstr "email"
|
||||
|
||||
#: mailbox_manager/models.py:309
|
||||
msgid "Mailbox"
|
||||
msgstr "Boîte mail"
|
||||
|
||||
#: mailbox_manager/models.py:310
|
||||
msgid "Mailboxes"
|
||||
msgstr "Boîtes mail"
|
||||
|
||||
#: mailbox_manager/models.py:352
|
||||
msgid "You can't create or update a mailbox for a disabled domain."
|
||||
msgstr "Vous ne pouvez pas créer ou mettre à jour une boîte mail pour un domaine désactivé."
|
||||
|
||||
#: mailbox_manager/models.py:411
|
||||
msgid "Mail domain invitation"
|
||||
msgstr "Invitation au domaine de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:412
|
||||
msgid "Mail domain invitations"
|
||||
msgstr "Invitations au domaine de messagerie"
|
||||
|
||||
#: mailbox_manager/models.py:441
|
||||
msgid "[La Suite] You have been invited to join La Régie"
|
||||
msgstr "[La Suite] Vous avez été invité(e) à rejoindre la Régie"
|
||||
|
||||
#: mailbox_manager/models.py:485
|
||||
msgid "destination address"
|
||||
msgstr "adresse de destination"
|
||||
|
||||
#: mailbox_manager/models.py:503
|
||||
msgid "Alias"
|
||||
msgstr "Alias"
|
||||
|
||||
#: mailbox_manager/models.py:504
|
||||
msgid "Aliases"
|
||||
msgstr "Alias"
|
||||
|
||||
#: mailbox_manager/utils/dimail.py:296
|
||||
msgid "Your new mailbox information"
|
||||
msgstr "Informations sur votre nouvelle boîte mail"
|
||||
|
||||
#: mailbox_manager/utils/dimail.py:307
|
||||
msgid "Your password has been updated"
|
||||
msgstr "Votre mot de passe a été mis à jour"
|
||||
|
||||
#: people/settings.py:159
|
||||
msgid "English"
|
||||
msgstr "Anglais"
|
||||
|
||||
#: people/settings.py:160
|
||||
msgid "French"
|
||||
msgstr "Français"
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ from requests import exceptions
|
||||
from mailbox_manager import enums, models
|
||||
from mailbox_manager.utils.dimail import DimailAPIClient
|
||||
|
||||
# Prevent Ruff complaining about mark_safe below
|
||||
# ruff: noqa: S308
|
||||
|
||||
|
||||
@admin.action(description=_("Import emails from dimail"))
|
||||
def sync_mailboxes_from_dimail(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
@@ -48,51 +51,6 @@ def sync_mailboxes_from_dimail(modeladmin, request, queryset): # pylint: disabl
|
||||
)
|
||||
|
||||
|
||||
@admin.action(description=_("Import aliases from dimail"))
|
||||
def sync_aliases_from_dimail(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
"""
|
||||
Admin action to import existing aliases from dimail.
|
||||
Checks alias is not a duplicate and that usernames don't clash with existing mailboxes.
|
||||
"""
|
||||
excluded_domains = []
|
||||
|
||||
client = DimailAPIClient()
|
||||
|
||||
for domain in queryset:
|
||||
if domain.status != enums.MailDomainStatusChoices.ENABLED:
|
||||
excluded_domains.append(domain.name)
|
||||
continue
|
||||
|
||||
try:
|
||||
imported_aliases = client.import_aliases(domain)
|
||||
except exceptions.HTTPError as err:
|
||||
messages.error(
|
||||
request,
|
||||
_("Synchronisation failed for %(domain)s with message: %(err)s")
|
||||
% {"domain": domain.name, "err": err},
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_(
|
||||
"Synchronisation succeed for %(domain)s.\
|
||||
Imported %(count_imported)s aliases: %(aliases)s"
|
||||
)
|
||||
% {
|
||||
"domain": domain.name,
|
||||
"count_imported": len(imported_aliases),
|
||||
"aliases": ", ".join(imported_aliases),
|
||||
},
|
||||
)
|
||||
|
||||
if excluded_domains:
|
||||
messages.warning(
|
||||
request,
|
||||
_("Sync require enabled domains. Excluded domains: %(domains)s")
|
||||
% {"domains": ", ".join(excluded_domains)},
|
||||
)
|
||||
|
||||
|
||||
@admin.action(description=_("Check and update status from dimail"))
|
||||
def fetch_domain_status_from_dimail(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
"""Admin action to check domain health with dimail and update domain status."""
|
||||
@@ -236,7 +194,6 @@ class MailDomainAdmin(admin.ModelAdmin):
|
||||
inlines = (UserMailDomainAccessInline,)
|
||||
actions = (
|
||||
sync_mailboxes_from_dimail,
|
||||
sync_aliases_from_dimail,
|
||||
fetch_domain_status_from_dimail,
|
||||
fetch_domain_expected_config_from_dimail,
|
||||
send_pending_mailboxes,
|
||||
@@ -250,8 +207,8 @@ class MailboxAdmin(UserAdmin):
|
||||
|
||||
list_display = ("__str__", "domain", "status", "updated_at")
|
||||
list_filter = ("status",)
|
||||
search_fields = ("local_part", "domain__name", "first_name", "last_name")
|
||||
readonly_fields = ["updated_at"]
|
||||
search_fields = ("local_part", "domain__name")
|
||||
readonly_fields = ["updated_at", "local_part", "domain"]
|
||||
|
||||
fieldsets = None
|
||||
add_fieldsets = (
|
||||
@@ -310,13 +267,3 @@ class MailDomainInvitationAdmin(admin.ModelAdmin):
|
||||
def is_expired(self, obj):
|
||||
"""Return the expiration date of the invitation."""
|
||||
return obj.is_expired
|
||||
|
||||
|
||||
@admin.register(models.Alias)
|
||||
class AliasAdmin(admin.ModelAdmin):
|
||||
"""Admin for alias model."""
|
||||
|
||||
list_display = ("local_part", "domain", "destination", "updated_at")
|
||||
list_filter = ("domain",)
|
||||
search_fields = ("local_part", "domain__name", "destination")
|
||||
readonly_fields = ["updated_at"]
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
from logging import getLogger
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core import exceptions as django_exceptions
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
from rest_framework import exceptions, serializers
|
||||
@@ -31,6 +29,7 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
"secondary_email",
|
||||
"status",
|
||||
]
|
||||
# everything is actually read-only as we do not allow update for now
|
||||
read_only_fields = ["id", "status"]
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -46,21 +45,12 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
"password": make_password(None), # generate an unusable password
|
||||
}
|
||||
)
|
||||
|
||||
if validated_data["domain"].status == enums.MailDomainStatusChoices.ENABLED:
|
||||
# send new mailbox request to dimail
|
||||
if mailbox.domain.status == enums.MailDomainStatusChoices.ENABLED:
|
||||
client = DimailAPIClient()
|
||||
try:
|
||||
response = client.create_mailbox(
|
||||
mailbox, self.context["request"].user.sub
|
||||
)
|
||||
except django_exceptions.ValidationError as exc:
|
||||
mailbox.delete()
|
||||
raise exc
|
||||
# send new mailbox request to dimail
|
||||
response = client.create_mailbox(mailbox, self.context["request"].user.sub)
|
||||
|
||||
mailbox.status = enums.MailDomainStatusChoices.ENABLED
|
||||
mailbox_data = response.json()
|
||||
mailbox.set_password(mailbox_data["password"])
|
||||
mailbox.save()
|
||||
|
||||
if mailbox.secondary_email:
|
||||
@@ -77,25 +67,10 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
mailbox,
|
||||
)
|
||||
|
||||
# actually save mailbox on our database
|
||||
return mailbox
|
||||
|
||||
|
||||
class MailboxUpdateSerializer(MailboxSerializer):
|
||||
"""A more restrictive serializer when updating mailboxes"""
|
||||
|
||||
class Meta:
|
||||
model = models.Mailbox
|
||||
fields = [
|
||||
"id",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"local_part",
|
||||
"secondary_email",
|
||||
"status",
|
||||
]
|
||||
read_only_fields = ("id", "local_part", "status")
|
||||
|
||||
|
||||
class MailDomainSerializer(serializers.ModelSerializer):
|
||||
"""Serialize mail domain."""
|
||||
|
||||
@@ -228,10 +203,23 @@ class MailDomainAccessSerializer(serializers.ModelSerializer):
|
||||
"You must set a domain slug in kwargs to create a new domain access."
|
||||
) from exc
|
||||
|
||||
# we're sure that an access exists. otherwise it would have failed at permissions.
|
||||
access = authenticated_user.mail_domain_accesses.get(
|
||||
domain__slug=domain_slug
|
||||
)
|
||||
try:
|
||||
access = authenticated_user.mail_domain_accesses.get(
|
||||
domain__slug=domain_slug
|
||||
)
|
||||
except models.MailDomainAccess.DoesNotExist as exc:
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to manage accesses for this domain."
|
||||
) from exc
|
||||
|
||||
# Authenticated user must be owner or admin of current domain to set new roles
|
||||
if access.role not in [
|
||||
enums.MailDomainRoleChoices.OWNER,
|
||||
enums.MailDomainRoleChoices.ADMIN,
|
||||
]:
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to manage accesses for this domain."
|
||||
)
|
||||
|
||||
# only an owner can set an owner role to another user
|
||||
if (
|
||||
@@ -289,41 +277,11 @@ class MailDomainInvitationSerializer(serializers.ModelSerializer):
|
||||
) from exc
|
||||
|
||||
domain = models.MailDomain.objects.get(slug=domain_slug)
|
||||
if not domain.get_abilities(user)["manage_accesses"]:
|
||||
raise exceptions.PermissionDenied(
|
||||
"You are not allowed to manage invitations for this domain."
|
||||
)
|
||||
|
||||
attrs["domain"] = domain
|
||||
attrs["issuer"] = user
|
||||
return attrs
|
||||
|
||||
|
||||
class AliasSerializer(serializers.ModelSerializer):
|
||||
"""Serialize aliases."""
|
||||
|
||||
domain = MailDomainSerializer(default="")
|
||||
|
||||
class Meta:
|
||||
model = models.Alias
|
||||
fields = [
|
||||
"id",
|
||||
"local_part",
|
||||
"destination",
|
||||
"domain",
|
||||
]
|
||||
read_only_fields = ["id", "domain"]
|
||||
|
||||
def validate_domain(self, value): # pylint: disable=unused-argument
|
||||
"""Forcefully set domain field to url domain."""
|
||||
return get_object_or_404(models.MailDomain, slug=self.context["domain_slug"])
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Override create function to fire a request to dimail on alias creation.
|
||||
"""
|
||||
if validated_data["domain"].status == enums.MailDomainStatusChoices.ENABLED:
|
||||
alias = models.Alias(**validated_data)
|
||||
|
||||
# send new alias request to dimail
|
||||
client = DimailAPIClient()
|
||||
client.create_alias(alias, self.context["request"].user.sub)
|
||||
return super().create(validated_data)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
"""API endpoints"""
|
||||
|
||||
from django.db.models import Q, Subquery
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import exceptions, filters, mixins, status, viewsets
|
||||
from rest_framework import exceptions, filters, mixins, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core import models as core_models
|
||||
from core.api.client.serializers import UserSerializer
|
||||
from core.exceptions import EmailAlreadyKnownException
|
||||
|
||||
from mailbox_manager import enums, models
|
||||
from mailbox_manager.api import permissions
|
||||
@@ -43,10 +40,7 @@ class MailDomainViewSet(
|
||||
Fetch domain status and expected config from dimail.
|
||||
"""
|
||||
|
||||
permission_classes = [
|
||||
permissions.DomainPermission,
|
||||
permissions.DomainResourcePermission,
|
||||
]
|
||||
permission_classes = [permissions.AccessPermission]
|
||||
serializer_class = serializers.MailDomainSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
ordering_fields = ["created_at", "name"]
|
||||
@@ -118,7 +112,7 @@ class MailDomainAccessViewSet(
|
||||
Delete targeted domain access
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.DomainResourcePermission]
|
||||
permission_classes = [permissions.MailDomainAccessRolePermission]
|
||||
serializer_class = serializers.MailDomainAccessSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
ordering_fields = ["role", "user__email", "user__name"]
|
||||
@@ -234,11 +228,9 @@ class MailDomainAccessViewSet(
|
||||
|
||||
|
||||
class MailBoxViewSet(
|
||||
viewsets.GenericViewSet,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""MailBox ViewSet
|
||||
|
||||
@@ -260,15 +252,9 @@ class MailBoxViewSet(
|
||||
|
||||
POST /api/<version>/mail-domains/<domain_slug>/mailboxes/<mailbox_id>/reset/
|
||||
Send a request to mail-provider to reset password.
|
||||
|
||||
PUT /api/<version>/mail-domains/<domain_slug>/mailboxes/<mailbox_id>/
|
||||
Send a request to update mailbox. Cannot modify domain or local_part.
|
||||
|
||||
PATCH /api/<version>/mail-domains/<domain_slug>/mailboxes/<mailbox_id>/
|
||||
Send a request to partially update mailbox. Cannot modify domain or local_part.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.DomainResourcePermission]
|
||||
permission_classes = [permissions.MailBoxPermission]
|
||||
serializer_class = serializers.MailboxSerializer
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
ordering = ["-created_at"]
|
||||
@@ -281,32 +267,6 @@ class MailBoxViewSet(
|
||||
return self.queryset.filter(domain__slug=domain_slug)
|
||||
return self.queryset
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extra context provided to the serializer class."""
|
||||
context = super().get_serializer_context()
|
||||
context["domain_slug"] = self.kwargs["domain_slug"]
|
||||
return context
|
||||
|
||||
def get_permissions(self):
|
||||
"""Add a specific permission for domain viewers to update their own mailbox."""
|
||||
if self.action in ["list", "retrieve"]:
|
||||
permission_classes = [permissions.DomainPermission]
|
||||
elif self.action in ["update", "partial_update"]:
|
||||
permission_classes = [
|
||||
permissions.DomainResourcePermission
|
||||
| permissions.IsMailboxOwnerPermission
|
||||
]
|
||||
else:
|
||||
return super().get_permissions()
|
||||
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Chooses list or detail serializer according to the action."""
|
||||
if self.action in {"update", "partial_update"}:
|
||||
return serializers.MailboxUpdateSerializer
|
||||
return self.serializer_class
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Create new mailbox."""
|
||||
domain_slug = self.kwargs.get("domain_slug", "")
|
||||
@@ -350,7 +310,6 @@ class MailDomainInvitationViewset(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""API ViewSet for user invitations to domain management.
|
||||
@@ -365,17 +324,13 @@ class MailDomainInvitationViewset(
|
||||
- issuer : User, automatically added from user making query, if allowed
|
||||
- domain : Domain, automatically added from requested URI
|
||||
Return a newly created invitation
|
||||
or an access if email is already linked to an existing user
|
||||
|
||||
PUT / PATCH : Not permitted. Instead of updating your invitation,
|
||||
delete and create a new one.
|
||||
|
||||
DELETE /api/<version>/mail-domains/<domain_slug>/invitations/:<invitation_id>/
|
||||
Delete targeted invitation
|
||||
"""
|
||||
|
||||
lookup_field = "id"
|
||||
permission_classes = [permissions.DomainResourcePermission]
|
||||
permission_classes = [permissions.AccessPermission]
|
||||
queryset = (
|
||||
models.MailDomainInvitation.objects.all()
|
||||
.select_related("domain")
|
||||
@@ -412,126 +367,3 @@ class MailDomainInvitationViewset(
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Attempt to create invitation. If user is already registered,
|
||||
they don't need an invitation but an access, which we create here."""
|
||||
email = request.data["email"]
|
||||
try:
|
||||
return super().create(request, *args, **kwargs)
|
||||
except EmailAlreadyKnownException as exc:
|
||||
user = models.User.objects.get(email__iexact=email)
|
||||
|
||||
models.MailDomainAccess.objects.create(
|
||||
user=user,
|
||||
domain=models.MailDomain.objects.get(slug=kwargs["domain_slug"]),
|
||||
role=request.data["role"],
|
||||
)
|
||||
raise exc
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def refresh(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||
"""Enable mailbox. Send a request to dimail and change status in our DB"""
|
||||
invitation = get_object_or_404(models.MailDomainInvitation, id=kwargs["id"])
|
||||
invitation.refresh()
|
||||
invitation.email_invitation()
|
||||
return Response(serializers.MailDomainInvitationSerializer(invitation).data)
|
||||
|
||||
|
||||
class AliasViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""API ViewSet for aliases.
|
||||
|
||||
GET /api/<version>/mail-domains/<domain_slug>/aliases/
|
||||
Return list of aliases related to that domain
|
||||
|
||||
POST /api/<version>/mail-domains/<domain_slug>/aliases/ with expected data:
|
||||
- local_part: str
|
||||
- destination: str
|
||||
Return a newly created alias
|
||||
|
||||
DELETE /api/<version>/mail-domains/<domain_slug>/aliases/<alias_pk>/
|
||||
Delete targeted alias
|
||||
|
||||
DELETE /api/<version>/mail-domains/<domain_slug>/aliases/?local_part=<local_part>/
|
||||
Delete all aliases of targeted local_part
|
||||
"""
|
||||
|
||||
lookup_field = "pk"
|
||||
permission_classes = [permissions.DomainResourcePermission]
|
||||
serializer_class = serializers.AliasSerializer
|
||||
queryset = models.Alias.objects.all().select_related("domain")
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
ordering_fields = ["local_part"]
|
||||
ordering = ["local_part"]
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Extra context provided to the serializer class."""
|
||||
context = super().get_serializer_context()
|
||||
context["domain_slug"] = self.kwargs["domain_slug"]
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(domain__slug=self.kwargs["domain_slug"])
|
||||
|
||||
if self.action == "list":
|
||||
# Determine which role the logged-in user has in the domain
|
||||
user_role_query = models.MailDomainAccess.objects.filter(
|
||||
user=self.request.user, domain__slug=self.kwargs["domain_slug"]
|
||||
).values("role")
|
||||
|
||||
queryset = (
|
||||
queryset
|
||||
# Abilities are computed based on logged-in user's role and
|
||||
# the user role on each domain access
|
||||
.annotate(user_role=Subquery(user_role_query)).distinct()
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""
|
||||
Override destroy method to delete specific alias and send request to dimail.
|
||||
"""
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
|
||||
client = DimailAPIClient()
|
||||
dimail_response = client.delete_alias(instance)
|
||||
|
||||
if dimail_response.status_code == status.HTTP_404_NOT_FOUND:
|
||||
return Response(
|
||||
"Domain out of sync with mailbox provider, please contact our support.",
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(methods=["DELETE"], detail=False)
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Bulk delete aliases. Filtering is required and accepted filter is local_part."""
|
||||
|
||||
if "local_part" not in self.request.query_params:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
local_part = self.request.query_params["local_part"]
|
||||
queryset = self.get_queryset().filter(
|
||||
local_part=local_part
|
||||
) # Manually call get_queryset to filter by domain and role
|
||||
if not queryset:
|
||||
raise Http404("No Alias matches the given query.")
|
||||
|
||||
# view is bounded to a domain, fetch is from the queryset to spare a dedicated DB request"
|
||||
domain_name = queryset[0].domain.name
|
||||
queryset.delete()
|
||||
|
||||
client = DimailAPIClient()
|
||||
client.delete_multiple_alias(local_part, domain_name)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
"""Permission handlers for the People mailbox manager app."""
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
from core.api.permissions import IsAuthenticated
|
||||
from core.api import permissions as core_permissions
|
||||
|
||||
from mailbox_manager import models
|
||||
|
||||
|
||||
class DomainPermission(IsAuthenticated):
|
||||
"""Permission class to manage mailboxes and aliases for a mail domain"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check permission based on domain."""
|
||||
|
||||
if not super().has_permission(request, view):
|
||||
return False
|
||||
|
||||
if not view.kwargs.get("domain_slug"):
|
||||
return True
|
||||
|
||||
domain = get_object_or_404(
|
||||
models.MailDomain,
|
||||
slug=view.kwargs.get("domain_slug", ""),
|
||||
accesses__user=request.user,
|
||||
)
|
||||
|
||||
abilities = domain.get_abilities(request.user)
|
||||
if request.method.lower() == "delete":
|
||||
return abilities.get("manage_accesses", False)
|
||||
|
||||
return abilities.get(request.method.lower(), False)
|
||||
|
||||
|
||||
class DomainResourcePermission(DomainPermission):
|
||||
class AccessPermission(core_permissions.IsAuthenticated):
|
||||
"""Permission class for access objects."""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
@@ -43,15 +14,20 @@ class DomainResourcePermission(DomainPermission):
|
||||
return abilities.get(request.method.lower(), False)
|
||||
|
||||
|
||||
class IsMailboxOwnerPermission(permissions.BasePermission):
|
||||
"""Authorize update for domain viewers on their own mailbox."""
|
||||
class MailBoxPermission(core_permissions.IsAuthenticated):
|
||||
"""Permission class to manage mailboxes for a mail domain"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""This permission is specifically about updates"""
|
||||
"""Check permission based on domain."""
|
||||
domain = models.MailDomain.objects.get(slug=view.kwargs.get("domain_slug", ""))
|
||||
abilities = domain.get_abilities(request.user)
|
||||
return abilities["get"]
|
||||
return abilities.get(request.method.lower(), False)
|
||||
|
||||
|
||||
class MailDomainAccessRolePermission(core_permissions.IsAuthenticated):
|
||||
"""Permission class to manage mailboxes for a mail domain"""
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""If the user is trying to update their own mailbox."""
|
||||
return obj.get_email() == request.user.email
|
||||
"""Check permission for a given object."""
|
||||
abilities = obj.get_abilities(request.user)
|
||||
return abilities.get(request.method.lower(), False)
|
||||
|
||||
@@ -15,5 +15,4 @@ class MailboxManagerConfig(AppConfig):
|
||||
"""
|
||||
Import signals when the app is ready.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel, unused-import
|
||||
import mailbox_manager.signals # noqa: PLC0415
|
||||
import mailbox_manager.signals # pylint: disable=import-outside-toplevel, unused-import
|
||||
|
||||
@@ -98,14 +98,3 @@ class MailDomainInvitationFactory(factory.django.DjangoModelFactory):
|
||||
[role[0] for role in enums.MailDomainRoleChoices.choices]
|
||||
)
|
||||
issuer = factory.SubFactory(core_factories.UserFactory)
|
||||
|
||||
|
||||
class AliasFactory(factory.django.DjangoModelFactory):
|
||||
"""A factory to create aliases."""
|
||||
|
||||
class Meta:
|
||||
model = models.Alias
|
||||
|
||||
domain = factory.SubFactory(MailDomainEnabledFactory)
|
||||
local_part = factory.Faker("word")
|
||||
destination = factory.Faker("email")
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 5.2.5 on 2025-09-08 12:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0025_alter_mailbox_secondary_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='mailbox',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='mailbox',
|
||||
constraint=models.UniqueConstraint(fields=('local_part', 'domain'), name='unique_username'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='mailbox',
|
||||
constraint=models.UniqueConstraint(fields=('first_name', 'last_name', 'domain'), name='unique_ox_display_name'),
|
||||
),
|
||||
]
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-21 15:49
|
||||
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0026_alter_mailbox_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='mailbox',
|
||||
name='unique_ox_display_name',
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='mailbox',
|
||||
constraint=models.UniqueConstraint(django.db.models.functions.text.Lower('first_name'), django.db.models.functions.text.Lower('last_name'), models.F('domain'), name='unique_ox_display_name', violation_error_message='Mailbox with this First name, Last name and Domain already exists.'),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-13 12:55
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0027_remove_mailbox_unique_ox_display_name_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Alias',
|
||||
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 at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')),
|
||||
('local_part', models.CharField(max_length=100)),
|
||||
('destination', models.CharField(max_length=254, validators=[django.core.validators.EmailValidator(allowlist=['localhost', 'devnull'])], verbose_name='destination address')),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='mailbox_manager.maildomain')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Alias',
|
||||
'verbose_name_plural': 'Aliases',
|
||||
'db_table': 'people_aliases',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('local_part', 'destination')},
|
||||
},
|
||||
),
|
||||
]
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-17 17:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0028_alias'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='alias',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='alias',
|
||||
constraint=models.UniqueConstraint(fields=('domain', 'local_part', 'destination'), name='no_duplicate'),
|
||||
),
|
||||
]
|
||||
@@ -9,15 +9,12 @@ from django.conf import settings
|
||||
from django.contrib.auth.base_user import AbstractBaseUser
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import exceptions, mail, validators
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db import models
|
||||
from django.db.models.functions import Lower
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import get_language, gettext, override
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.exceptions import EmailAlreadyKnownException
|
||||
from core.models import BaseInvitation, BaseModel, Organization, User
|
||||
|
||||
from mailbox_manager.enums import (
|
||||
@@ -91,8 +88,6 @@ class MailDomain(BaseModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
objects = models.Manager()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Override save function to compute the slug."""
|
||||
self.slug = self.get_slug()
|
||||
@@ -111,7 +106,7 @@ class MailDomain(BaseModel):
|
||||
if user.is_authenticated:
|
||||
try:
|
||||
role = self.accesses.filter(user=user).values("role")[0]["role"]
|
||||
except MailDomainAccess.DoesNotExist, IndexError:
|
||||
except (MailDomainAccess.DoesNotExist, IndexError):
|
||||
role = None
|
||||
|
||||
is_owner_or_admin = role in [
|
||||
@@ -224,7 +219,7 @@ class MailDomainAccess(BaseModel):
|
||||
authenticated_user_role = user.mail_domain_accesses.get(
|
||||
domain=self.domain
|
||||
).role
|
||||
except MailDomainAccess.DoesNotExist, IndexError:
|
||||
except (MailDomainAccess.DoesNotExist, IndexError):
|
||||
return []
|
||||
|
||||
# only an owner can set an owner role
|
||||
@@ -252,7 +247,7 @@ class MailDomainAccess(BaseModel):
|
||||
if user.is_authenticated:
|
||||
try:
|
||||
role = user.mail_domain_accesses.filter(domain=self.domain).get().role
|
||||
except MailDomainAccess.DoesNotExist, IndexError:
|
||||
except (MailDomainAccess.DoesNotExist, IndexError):
|
||||
role = None
|
||||
|
||||
is_owner_or_admin = role in [
|
||||
@@ -308,23 +303,7 @@ class Mailbox(AbstractBaseUser, BaseModel):
|
||||
db_table = "people_mail_box"
|
||||
verbose_name = _("Mailbox")
|
||||
verbose_name_plural = _("Mailboxes")
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["local_part", "domain"], name="unique_username"
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
Lower("first_name"),
|
||||
Lower("last_name"),
|
||||
"domain",
|
||||
name="unique_ox_display_name",
|
||||
violation_error_message="Mailbox with this First name, \
|
||||
Last name and Domain already exists.",
|
||||
),
|
||||
# Display name in OpenXChange must be unique
|
||||
# To avoid sending failing requests to dimail,
|
||||
# we impose uniqueness here too
|
||||
# And compare to lowercase to enforce case-insensitive uniqueness
|
||||
]
|
||||
unique_together = ("local_part", "domain")
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
@@ -351,8 +330,6 @@ Last name and Domain already exists.",
|
||||
raise exceptions.ValidationError(
|
||||
_("You can't create or update a mailbox for a disabled domain.")
|
||||
)
|
||||
|
||||
self.local_part = self.local_part.lower()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@@ -364,25 +341,6 @@ Last name and Domain already exists.",
|
||||
"""Return the email address of the mailbox."""
|
||||
return f"{self.local_part}@{self.domain.name}"
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""Compute and return abilities for a given user."""
|
||||
role = user.mail_domain_accesses.get(domain=self.domain).role
|
||||
|
||||
is_owner_or_admin = role in [
|
||||
MailDomainRoleChoices.OWNER,
|
||||
MailDomainRoleChoices.ADMIN,
|
||||
]
|
||||
|
||||
is_self = self.get_email() == user.email
|
||||
|
||||
return {
|
||||
"get": bool(role),
|
||||
"post": is_owner_or_admin,
|
||||
"patch": is_owner_or_admin or is_self,
|
||||
"put": is_owner_or_admin or is_self,
|
||||
"delete": False,
|
||||
}
|
||||
|
||||
|
||||
class MailDomainInvitation(BaseInvitation):
|
||||
"""User invitation to mail domains."""
|
||||
@@ -416,23 +374,6 @@ class MailDomainInvitation(BaseInvitation):
|
||||
)
|
||||
]
|
||||
|
||||
def refresh(self):
|
||||
"""Create domain access if refresh fail because user was created
|
||||
after invitation expiration"""
|
||||
try:
|
||||
super().refresh()
|
||||
except EmailAlreadyKnownException as exc:
|
||||
user = User.objects.get(email__iexact=self.email)
|
||||
|
||||
MailDomainAccess.objects.create(
|
||||
user=user,
|
||||
domain=self.domain,
|
||||
role=self.role,
|
||||
)
|
||||
|
||||
self.delete() # delete related invitation
|
||||
raise exc
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.email} invited to {self.domain}"
|
||||
|
||||
@@ -461,7 +402,7 @@ class MailDomainInvitation(BaseInvitation):
|
||||
role = self.domain.accesses.filter(user=user).values("role")[0][
|
||||
"role"
|
||||
]
|
||||
except self._meta.model.DoesNotExist, IndexError:
|
||||
except (self._meta.model.DoesNotExist, IndexError):
|
||||
role = None
|
||||
|
||||
can_delete = role in [
|
||||
@@ -475,62 +416,3 @@ class MailDomainInvitation(BaseInvitation):
|
||||
"patch": False,
|
||||
"put": False,
|
||||
}
|
||||
|
||||
|
||||
class Alias(BaseModel):
|
||||
"""Model for aliases."""
|
||||
|
||||
local_part = models.CharField(max_length=100, blank=False)
|
||||
destination = models.CharField(
|
||||
_("destination address"),
|
||||
max_length=254,
|
||||
null=False,
|
||||
blank=False,
|
||||
validators=[
|
||||
EmailValidator(allowlist=["localhost", "devnull"]),
|
||||
],
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
MailDomain,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="aliases",
|
||||
null=False,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "people_aliases"
|
||||
verbose_name = _("Alias")
|
||||
verbose_name_plural = _("Aliases")
|
||||
ordering = ["-created_at"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["domain", "local_part", "destination"], name="no_duplicate"
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.local_part} to {self.destination}"
|
||||
|
||||
def get_abilities(self, user):
|
||||
"""Compute and return abilities for a given user. Admin and owners can
|
||||
edit aliases, but also viewer if the alias points to their email."""
|
||||
try:
|
||||
role = user.mail_domain_accesses.get(domain=self.domain).role
|
||||
except MailDomainAccess.DoesNotExist, IndexError:
|
||||
role = None
|
||||
|
||||
is_owner_or_admin = role in [
|
||||
MailDomainRoleChoices.OWNER,
|
||||
MailDomainRoleChoices.ADMIN,
|
||||
]
|
||||
|
||||
is_self = self.destination == user.email
|
||||
|
||||
return {
|
||||
"get": bool(role),
|
||||
"post": is_owner_or_admin,
|
||||
"patch": False,
|
||||
"put": False,
|
||||
"delete": is_owner_or_admin or is_self,
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
"""
|
||||
Tests for aliases API endpoint in People's app mailbox_manager.
|
||||
Focus on "bulk delete" action.
|
||||
"""
|
||||
# pylint: disable=W0613
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_aliases_bulk_delete__anonymous_get_401():
|
||||
"""Anonymous user should not be able to bulk delete."""
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
alias_, _, _ = factories.AliasFactory.create_batch(3, domain=mail_domain)
|
||||
|
||||
client = APIClient()
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/?local_part={alias_.local_part}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert models.Alias.objects.count() == 3
|
||||
|
||||
|
||||
def test_api_aliases_bulk_delete__no_access_get_404():
|
||||
"""User with no access to domain should not be able to bulk delete."""
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
alias_, _, _ = factories.AliasFactory.create_batch(3, domain=mail_domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/?local_part={alias_.local_part}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert models.Alias.objects.count() == 3
|
||||
|
||||
|
||||
def test_api_aliases_bulk_delete__viewer_get_403():
|
||||
"""Viewer user should not be able to bulk delete."""
|
||||
access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.VIEWER)
|
||||
alias_, _, _ = factories.AliasFactory.create_batch(3, domain=access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/?local_part={alias_.local_part}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.Alias.objects.count() == 3
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_aliases_bulk_delete__administrators_allowed_all_destination(
|
||||
dimail_token_ok,
|
||||
):
|
||||
"""
|
||||
Administrators of a domain should be allowed to bulk delete all aliases
|
||||
of a given local_part.
|
||||
"""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory(
|
||||
users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)]
|
||||
)
|
||||
alias_ = factories.AliasFactory(domain=mail_domain)
|
||||
factories.AliasFactory.create_batch(
|
||||
2, domain=mail_domain, local_part=alias_.local_part
|
||||
)
|
||||
|
||||
# additional aliases that shouldn't be affected
|
||||
factories.AliasFactory.create_batch(
|
||||
2, domain=mail_domain, destination=alias_.destination
|
||||
)
|
||||
factories.AliasFactory(
|
||||
local_part=alias_.local_part,
|
||||
destination=alias_.destination,
|
||||
)
|
||||
|
||||
# Mock dimail response
|
||||
responses.delete(
|
||||
re.compile(r".*/aliases/"),
|
||||
status=status.HTTP_204_NO_CONTENT,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(authenticated_user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/?local_part={alias_.local_part}",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert models.Alias.objects.count() == 3
|
||||
assert not models.Alias.objects.filter(
|
||||
domain=mail_domain, local_part=alias_.local_part
|
||||
).exists()
|
||||
|
||||
|
||||
def test_api_aliases_bulk_delete__no_local_part_bad_request():
|
||||
"""Filtering by local part is mandatory when bulk deleting aliases."""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory(
|
||||
users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)]
|
||||
)
|
||||
alias_ = factories.AliasFactory(domain=mail_domain)
|
||||
factories.AliasFactory.create_batch(
|
||||
2, domain=mail_domain, local_part=alias_.local_part
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(authenticated_user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert models.Alias.objects.count() == 3
|
||||
@@ -1,214 +0,0 @@
|
||||
"""
|
||||
Tests for aliases API endpoint.
|
||||
Focus on "create" action.
|
||||
"""
|
||||
# pylint: disable=W0613
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_aliases_create__anonymous():
|
||||
"""Anonymous user should not create aliases"""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/aliases/",
|
||||
{"whatever": "this should not be updated"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert not models.Alias.objects.exists()
|
||||
|
||||
|
||||
def test_api_aliases_create__no_access_forbidden_not_found():
|
||||
"""Unrelated user not having domain permission should not create aliases."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/aliases/",
|
||||
{"local_part": "intrusive", "destination": "intrusive@mail.com"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert not models.Alias.objects.exists()
|
||||
|
||||
|
||||
def test_api_aliases_create__viewer_forbidden():
|
||||
"""Domain viewers should not create aliases."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
access = factories.MailDomainAccessFactory(role="viewer", domain=domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/aliases/",
|
||||
{"local_part": "intrusive", "destination": "intrusive@mail.com"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert not models.Alias.objects.exists()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_aliases_create__duplicate_forbidden():
|
||||
"""Cannot create alias if existing alias same domain + local part + destination."""
|
||||
access = factories.MailDomainAccessFactory(
|
||||
role="owner", domain=factories.MailDomainEnabledFactory()
|
||||
)
|
||||
|
||||
existing_alias = factories.AliasFactory(domain=access.domain)
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
|
||||
{
|
||||
"local_part": existing_alias.local_part,
|
||||
"destination": existing_alias.destination,
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert models.Alias.objects.filter(domain=access.domain).count() == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_aliases_create__async_alias_bad_request(dimail_token_ok):
|
||||
"""
|
||||
If People fall out of sync with dimail, return a clear error if alias cannot be created
|
||||
because it already exists on dimail.
|
||||
"""
|
||||
access = factories.MailDomainAccessFactory(
|
||||
role="owner", domain=factories.MailDomainEnabledFactory()
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
# Mock dimail response
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(r".*/aliases/"),
|
||||
body=json.dumps({"detail": "Alias already exists"}),
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
|
||||
{
|
||||
"local_part": "already_existing_alias",
|
||||
"destination": "someone@outsidedomain.com",
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json() == {
|
||||
"NON_FIELD_ERRORS": [
|
||||
"Alias already exists. Your domain is out of sync, please contact our support."
|
||||
]
|
||||
}
|
||||
assert not models.Alias.objects.exists()
|
||||
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
[enums.MailDomainRoleChoices.OWNER, enums.MailDomainRoleChoices.ADMIN],
|
||||
)
|
||||
def test_api_aliases_create__admins_ok(role, dimail_token_ok):
|
||||
"""Domain admins should be able to create aliases."""
|
||||
access = factories.MailDomainAccessFactory(role=role)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
# Prepare responses
|
||||
# token response in fixtures
|
||||
responses.post(
|
||||
re.compile(rf".*/domains/{access.domain.name}/aliases/"),
|
||||
json={
|
||||
"username": "contact",
|
||||
"domain": access.domain.name,
|
||||
"destination": "someone@outsidedomain.com",
|
||||
"allow_to_send": True,
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
|
||||
{"local_part": "contact", "destination": "someone@outsidedomain.com"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
alias = models.Alias.objects.get()
|
||||
assert alias.local_part == "contact"
|
||||
assert alias.destination == "someone@outsidedomain.com"
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_aliases_create__existing_mailbox_ok(dimail_token_ok):
|
||||
"""Can create alias even if local_part is already used by a mailbox."""
|
||||
access = factories.MailDomainAccessFactory(
|
||||
role="owner", domain=factories.MailDomainEnabledFactory()
|
||||
)
|
||||
mailbox = factories.MailboxFactory(domain=access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
|
||||
responses.post(
|
||||
re.compile(rf".*/domains/{access.domain.name}/aliases/"),
|
||||
json={
|
||||
"username": mailbox.local_part,
|
||||
"domain": access.domain.name,
|
||||
"destination": "someone@outsidedomain.com",
|
||||
"allow_to_send": False,
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
|
||||
{"local_part": mailbox.local_part, "destination": "someone@outsidedomain.com"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert models.Alias.objects.exists()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_aliases_create__devnull_destination_ok(dimail_token_ok):
|
||||
"""Can create alias where destination is devnull@devnull."""
|
||||
access = factories.MailDomainAccessFactory(
|
||||
role="owner", domain=factories.MailDomainEnabledFactory()
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
|
||||
responses.post(
|
||||
re.compile(rf".*/domains/{access.domain.name}/aliases/"),
|
||||
json={
|
||||
"username": "spammy-address",
|
||||
"domain": access.domain.name,
|
||||
"destination": "devnull@devnull",
|
||||
"allow_to_send": False,
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
|
||||
{"local_part": "spammy-address", "destination": "devnull@devnull"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert models.Alias.objects.exists()
|
||||
@@ -1,139 +0,0 @@
|
||||
"""
|
||||
Tests for aliases API endpoint in People's app mailbox_manager.
|
||||
Focus on "delete" action.
|
||||
"""
|
||||
# pylint: disable=W0613
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_aliases_delete__anonymous():
|
||||
"""Anonymous user should not be able to delete aliases."""
|
||||
alias_ = factories.AliasFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/mail-domains/{alias_.domain.slug}/aliases/{alias_.pk}/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert models.Alias.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_aliases_delete__no_access_forbidden_not_found():
|
||||
"""
|
||||
Authenticated users should not be allowed to delete an alias in a
|
||||
mail domain to which they are not related.
|
||||
"""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
alias_ = factories.AliasFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(authenticated_user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{alias_.domain.slug}/aliases/{alias_.pk}/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert models.Alias.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_aliases_delete__viewer_forbidden():
|
||||
"""
|
||||
Authenticated users should not be allowed to delete aliases for a
|
||||
mail domain in which they are a simple viewer.
|
||||
"""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory(
|
||||
users=[(authenticated_user, enums.MailDomainRoleChoices.VIEWER)]
|
||||
)
|
||||
alias_ = factories.AliasFactory(domain=mail_domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(authenticated_user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/{alias_.pk}/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.Alias.objects.count() == 1
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_aliases_delete__administrators_allowed(dimail_token_ok):
|
||||
"""
|
||||
Administrators of a mail domain should be allowed to delete aliases.
|
||||
"""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory(
|
||||
users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)]
|
||||
)
|
||||
alias_ = factories.AliasFactory(domain=mail_domain)
|
||||
factories.AliasFactory.create_batch(
|
||||
2, domain=mail_domain, local_part=alias_.local_part
|
||||
)
|
||||
|
||||
# additional aliases that shouldn't be affected
|
||||
factories.AliasFactory.create_batch(
|
||||
2, domain=mail_domain, destination=alias_.destination
|
||||
)
|
||||
factories.AliasFactory(
|
||||
local_part=alias_.local_part,
|
||||
destination=alias_.destination,
|
||||
)
|
||||
|
||||
# Mock dimail response
|
||||
responses.delete(
|
||||
re.compile(r".*/aliases/"),
|
||||
status=status.HTTP_204_NO_CONTENT,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(authenticated_user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/{alias_.pk}/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert models.Alias.objects.count() == 5
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_aliases_delete__404_out_of_sync(dimail_token_ok):
|
||||
"""
|
||||
Should return a clear message when dimail returns 404 not found.
|
||||
"""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory(
|
||||
users=[(authenticated_user, enums.MailDomainRoleChoices.ADMIN)]
|
||||
)
|
||||
alias_ = factories.AliasFactory(domain=mail_domain)
|
||||
# Mock dimail response
|
||||
responses.delete(
|
||||
re.compile(r".*/aliases/"),
|
||||
json={"detail": "Not found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(authenticated_user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/aliases/{alias_.pk}/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert (
|
||||
response.json()
|
||||
== "Domain out of sync with mailbox provider, please contact our support."
|
||||
)
|
||||
assert not models.Alias.objects.exists()
|
||||
@@ -1,65 +0,0 @@
|
||||
"""
|
||||
Tests for aliases API endpoint in People's app mailbox_manager.
|
||||
Focus on "list" action.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_aliases_list__anonymous():
|
||||
"""Anonymous user should not be able to list aliases"""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
factories.AliasFactory.create_batch(3, domain=domain)
|
||||
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/aliases/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_aliases_list__no_access_forbidden_not_found():
|
||||
"""User authenticated but not having domain permission should not list aliases."""
|
||||
factories.MailDomainAccessFactory() # access to another domain
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
factories.AliasFactory.create_batch(3, domain=domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/aliases/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
[
|
||||
enums.MailDomainRoleChoices.OWNER,
|
||||
enums.MailDomainRoleChoices.ADMIN,
|
||||
enums.MailDomainRoleChoices.VIEWER,
|
||||
],
|
||||
)
|
||||
def test_api_aliases_list__authorized_ok(role):
|
||||
"""Domain viewers and admins should be able to list aliases."""
|
||||
access = factories.MailDomainAccessFactory(role=role)
|
||||
factories.AliasFactory.create_batch(2, local_part="support", domain=access.domain)
|
||||
factories.AliasFactory.create_batch(3, domain=access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/aliases/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["results"] == sorted(
|
||||
response.json()["results"], key=lambda x: x["local_part"]
|
||||
)
|
||||
assert response.json()["count"] == 5
|
||||
+47
-78
@@ -11,7 +11,8 @@ from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
from mailbox_manager import enums, factories
|
||||
from mailbox_manager.api.client import serializers
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -19,12 +20,13 @@ pytestmark = pytest.mark.django_db
|
||||
def test_api_domain_invitations__create__anonymous():
|
||||
"""Anonymous users should not be able to create invitations."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
invitation_values = serializers.MailDomainInvitationSerializer(
|
||||
factories.MailDomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
{
|
||||
"email": "some.email@domain.com",
|
||||
"role": "admin",
|
||||
},
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
@@ -33,23 +35,24 @@ def test_api_domain_invitations__create__anonymous():
|
||||
}
|
||||
|
||||
|
||||
def test_api_domain_invitations__create__no_access_forbidden_not_found():
|
||||
def test_api_domain_invitations__create__authenticated_outsider():
|
||||
"""Users should not be permitted to send domain management invitations
|
||||
for a domain they don't manage."""
|
||||
user = core_factories.UserFactory()
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
invitation_values = serializers.MailDomainInvitationSerializer(
|
||||
factories.MailDomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
{
|
||||
"email": "some.email@domain.com",
|
||||
"role": "viewer",
|
||||
},
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -62,25 +65,28 @@ def test_api_domain_invitations__admin_should_create_invites(role):
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
factories.MailDomainAccessFactory(domain=domain, user=user, role=role)
|
||||
|
||||
invitation_values = serializers.MailDomainInvitationSerializer(
|
||||
factories.MailDomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
{
|
||||
"email": "some.email@domain.com",
|
||||
"role": "owner",
|
||||
},
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert len(mail.outbox) == 1
|
||||
email = mail.outbox[0]
|
||||
assert email.to == ["some.email@domain.com"]
|
||||
assert email.to == [invitation_values["email"]]
|
||||
assert email.subject == "[La Suite] Vous avez été invité(e) à rejoindre la Régie"
|
||||
|
||||
|
||||
def test_api_domain_invitations__no_access_forbidden_not_found():
|
||||
def test_api_domain_invitations__viewers_should_not_invite_to_manage_domains():
|
||||
"""
|
||||
Domain viewers should not be able to invite new domain managers.
|
||||
"""
|
||||
@@ -90,19 +96,21 @@ def test_api_domain_invitations__no_access_forbidden_not_found():
|
||||
domain=domain, user=user, role=enums.MailDomainRoleChoices.VIEWER
|
||||
)
|
||||
|
||||
invitation_values = serializers.MailDomainInvitationSerializer(
|
||||
factories.MailDomainInvitationFactory.build()
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
{
|
||||
"email": "some.email@domain.com",
|
||||
"role": "viewer",
|
||||
},
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
"detail": "You are not allowed to manage invitations for this domain."
|
||||
}
|
||||
|
||||
|
||||
@@ -117,81 +125,42 @@ def test_api_domain_invitations__should_not_create_duplicate_invitations():
|
||||
domain=domain, user=user, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
# New invitation to the same domain with the exact same email address
|
||||
# Create a new invitation to the same domain with the exact same email address
|
||||
duplicated_invitation = serializers.MailDomainInvitationSerializer(
|
||||
factories.MailDomainInvitationFactory.build(email=existing_invitation.email)
|
||||
).data
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/",
|
||||
{
|
||||
"email": existing_invitation.email,
|
||||
"role": "owner",
|
||||
},
|
||||
duplicated_invitation,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["__all__"] == [
|
||||
"Mail domain invitation with this Email address and Domain already exists."
|
||||
]
|
||||
assert models.MailDomainInvitation.objects.count() == 1 # and specifically, not 2
|
||||
|
||||
|
||||
def test_api_domain_invitations__inviting_known_email_should_create_access():
|
||||
def test_api_domain_invitations__should_not_invite_when_user_already_exists():
|
||||
"""Already existing users should not be invited but given access directly."""
|
||||
existing_user = core_factories.UserFactory()
|
||||
|
||||
# Grant privileged role on the domain to the user
|
||||
access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
invitation_values = serializers.MailDomainInvitationSerializer(
|
||||
factories.MailDomainInvitationFactory.build(email=existing_user.email)
|
||||
).data
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/invitations/",
|
||||
{
|
||||
"email": existing_user.email,
|
||||
"role": "owner",
|
||||
},
|
||||
invitation_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert (
|
||||
response.json()["detail"]
|
||||
== "Email already known. Invitation not sent but access created instead."
|
||||
)
|
||||
|
||||
assert not models.MailDomainInvitation.objects.exists()
|
||||
assert models.MailDomainAccess.objects.filter(user=existing_user).exists()
|
||||
|
||||
|
||||
def test_api_domain_invitations__inviting_known_email_case_insensitive():
|
||||
"""Email matching should be case-insensitive when creating access for existing users."""
|
||||
# Create a user with lowercase email
|
||||
existing_user = core_factories.UserFactory(email="john.doe@example.com")
|
||||
access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
|
||||
# Try to invite with same email different case - should create access for existing user
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/invitations/",
|
||||
{
|
||||
"email": "John.Doe@Example.COM",
|
||||
"role": "owner",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert (
|
||||
response.json()["detail"]
|
||||
== "Email already known. Invitation not sent but access created instead."
|
||||
)
|
||||
|
||||
# No invitation should be created
|
||||
assert not models.MailDomainInvitation.objects.exists()
|
||||
|
||||
# Access should be created for the existing user (not a new user)
|
||||
assert models.MailDomainAccess.objects.filter(user=existing_user).exists()
|
||||
assert models.MailDomainAccess.objects.filter(
|
||||
user=existing_user, domain=access.domain
|
||||
).exists()
|
||||
|
||||
# Ensure only one user exists (no duplicate user created)
|
||||
assert models.User.objects.filter(email__iexact="john.doe@example.com").count() == 1
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["email"] == [
|
||||
"This email is already associated to a registered user."
|
||||
]
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
"""
|
||||
Tests for MailDomainInvitation API endpoint in People's app mailbox_manager.
|
||||
Focus on "delete" action.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from mailbox_manager import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_domain_invitations__delete__anonymous():
|
||||
"""Anonymous users should not be able to delete invitations."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
|
||||
response = APIClient().delete(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
assert models.MailDomainInvitation.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_invitations__delete__no_access_not_found():
|
||||
"""Users should not be permitted to delete invitations
|
||||
on domains they don't manage."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
|
||||
other_access = factories.MailDomainAccessFactory(role="owner") # unrelated access
|
||||
client = APIClient()
|
||||
client.force_login(other_access.user)
|
||||
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert models.MailDomainInvitation.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_invitations__delete__viewers_forbidden():
|
||||
"""Domain viewers should not be permitted to delete invitations."""
|
||||
access = factories.MailDomainAccessFactory(role="viewer")
|
||||
invitation = factories.MailDomainInvitationFactory(domain=access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.MailDomainInvitation.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["owner", "administrator"],
|
||||
)
|
||||
def test_api_domain_invitations__delete_admins_ok(role):
|
||||
"""Domain owners and admins should be able to delete invitations."""
|
||||
access = factories.MailDomainAccessFactory(role=role)
|
||||
invitation = factories.MailDomainInvitationFactory(domain=access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.delete(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/invitations/{invitation.id}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert not models.MailDomainInvitation.objects.exists()
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
"""
|
||||
Tests for MailDomainInvitations API endpoint in People's app mailbox_manager.
|
||||
Focus on "refresh" action.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_domain_invitations__anonymous_cannot_refresh():
|
||||
"""Anonymous are not allowed to refresh."""
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_domain_invitations__no_access_cannot_see_invitation():
|
||||
"""Users with no permission on the domain should be returned a 404 not found."""
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
|
||||
otro_access = factories.MailDomainAccessFactory(
|
||||
role=enums.MailDomainRoleChoices.ADMIN
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(otro_access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
def test_api_domain_invitations__viewer_cannot_refresh():
|
||||
"""Viewers cannot refresh invitations."""
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=invitation.domain, role=enums.MailDomainRoleChoices.VIEWER
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
def test_api_domain_invitations__admins_can_refresh_expired_invitations():
|
||||
"""Admins can refresh invitations."""
|
||||
with freeze_time("2025-12-16"):
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=invitation.domain, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
assert invitation.is_expired is True # check invitation is correctly expired
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
# can refresh expired invitations
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["is_expired"] is False
|
||||
assert models.MailDomainInvitation.objects.count() == 1
|
||||
|
||||
invitation.refresh_from_db()
|
||||
|
||||
# can also refresh valid
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["is_expired"] is False
|
||||
assert models.MailDomainInvitation.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_invitations__refreshing_invitation_can_convert_to_access():
|
||||
"""In the event of someone creating their account after their invitation expired,
|
||||
said invitation has not been converted to access.
|
||||
We don't want to create a new invitation but create an access instead."""
|
||||
with freeze_time("2025-12-16"):
|
||||
invitation = factories.MailDomainInvitationFactory()
|
||||
core_factories.UserFactory(email=invitation.email)
|
||||
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=invitation.domain, role=enums.MailDomainRoleChoices.OWNER
|
||||
)
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/{invitation.id}/refresh/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert (
|
||||
response.json()["detail"]
|
||||
== "Email already known. Invitation not sent but access created instead."
|
||||
)
|
||||
assert models.MailDomainAccess.objects.count() == 2
|
||||
assert not models.MailDomainInvitation.objects.exists()
|
||||
+3
-3
@@ -26,7 +26,7 @@ def test_api_domain_invitations__anonymous_user_should_not_retrieve_invitations(
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_domain_invitations__no_access_forbidden_not_found():
|
||||
def test_api_domain_invitations__unrelated_user_should_not_retrieve_invitations():
|
||||
"""
|
||||
Authenticated unrelated users should not be able to retrieve invitations.
|
||||
"""
|
||||
@@ -40,8 +40,8 @@ def test_api_domain_invitations__no_access_forbidden_not_found():
|
||||
f"/api/v1.0/mail-domains/{invitation.domain.slug}/invitations/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "No MailDomain matches the given query."}
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 0
|
||||
|
||||
|
||||
def test_api_domain_invitations__domain_managers_should_list_invitations():
|
||||
|
||||
@@ -41,14 +41,17 @@ def test_api_mail_domains__create_name_unique():
|
||||
"""
|
||||
Creating domain should raise an error if already existing name.
|
||||
"""
|
||||
existing_domain = factories.MailDomainFactory()
|
||||
factories.MailDomainFactory(name="existing_domain.com")
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{"name": existing_domain.name},
|
||||
{
|
||||
"name": "existing_domain.com",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Tests for MailDomains API endpoint in People's mailbox manager app. Focus on "fetch" action.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
@@ -81,7 +82,7 @@ def test_api_mail_domains__fetch_from_dimail__viewer():
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
def test_api_mail_domains__fetch_from_dimail_admin_successful(role):
|
||||
def test_api_mail_domains__fetch_from_dimail(role):
|
||||
"""
|
||||
Authenticated users should be allowed to fetch a domain
|
||||
from dimail if they are an owner or admin.
|
||||
|
||||
@@ -69,7 +69,7 @@ def test_api_mail_domains__retrieve_authenticated_unrelated():
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mail_domains__retrieve_authenticated_related_successful():
|
||||
def test_api_mail_domains__retrieve_authenticated_related():
|
||||
"""
|
||||
Authenticated users should be allowed to retrieve a domain
|
||||
to which they have access.
|
||||
|
||||
+5
-5
@@ -35,7 +35,7 @@ def test_api_mail_domain__accesses_create_anonymous():
|
||||
assert models.MailDomainAccess.objects.exists() is False
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_create_no_access_forbidden_not_found():
|
||||
def test_api_mail_domain__accesses_create_authenticated_unrelated():
|
||||
"""
|
||||
Authenticated users should not be allowed to create domain accesses for a domain to
|
||||
which they are not related.
|
||||
@@ -56,14 +56,14 @@ def test_api_mail_domain__accesses_create_no_access_forbidden_not_found():
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {
|
||||
"detail": "No MailDomain matches the given query.",
|
||||
"detail": "You are not allowed to manage accesses for this domain."
|
||||
}
|
||||
assert not models.MailDomainAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_create_viewer_forbidden():
|
||||
def test_api_mail_domain__accesses_create_authenticated_viewer():
|
||||
"""Viewer of a mail domain should not be allowed to create mail domain accesses."""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory(
|
||||
@@ -85,7 +85,7 @@ def test_api_mail_domain__accesses_create_viewer_forbidden():
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action.",
|
||||
"detail": "You are not allowed to manage accesses for this domain."
|
||||
}
|
||||
|
||||
assert not models.MailDomainAccess.objects.filter(user=other_user).exists()
|
||||
|
||||
+7
-7
@@ -13,7 +13,7 @@ from mailbox_manager import enums, factories, models
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_domain_accesses_delete__anonymous_forbidden():
|
||||
def test_api_mail_domain__accesses_delete_anonymous():
|
||||
"""Anonymous users should not be allowed to destroy a mail domain access."""
|
||||
access = factories.MailDomainAccessFactory()
|
||||
|
||||
@@ -25,7 +25,7 @@ def test_api_domain_accesses_delete__anonymous_forbidden():
|
||||
assert models.MailDomainAccess.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_accesses_delete__unrelated_notfound():
|
||||
def test_api_mail_domain__accesses_delete_authenticated():
|
||||
"""
|
||||
Authenticated users should not be allowed to delete a mail domain access for a
|
||||
mail domain to which they are not related.
|
||||
@@ -39,11 +39,11 @@ def test_api_domain_accesses_delete__unrelated_notfound():
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/accesses/{access.id!s}/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.MailDomainAccess.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_accesses_delete__viewer_forbidden():
|
||||
def test_api_mail_domain__accesses_delete_viewer():
|
||||
"""
|
||||
Authenticated users should not be allowed to delete a mail domain access for a
|
||||
mail domain in which they are a simple viewer.
|
||||
@@ -65,7 +65,7 @@ def test_api_domain_accesses_delete__viewer_forbidden():
|
||||
assert models.MailDomainAccess.objects.filter(user=access.user).exists()
|
||||
|
||||
|
||||
def test_api_domain_accesses_delete__administrators_successful():
|
||||
def test_api_mail_domain__accesses_delete_administrators():
|
||||
"""
|
||||
Administrators of a mail domain should be allowed to delete accesses excepted owner accesses.
|
||||
"""
|
||||
@@ -89,7 +89,7 @@ def test_api_domain_accesses_delete__administrators_successful():
|
||||
assert models.MailDomainAccess.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_accesses_delete__owners_successful():
|
||||
def test_api_mail_domain__accesses_delete_owners():
|
||||
"""
|
||||
An owner should be able to delete the mail domain access of another user including
|
||||
a owner access.
|
||||
@@ -114,7 +114,7 @@ def test_api_domain_accesses_delete__owners_successful():
|
||||
assert models.MailDomainAccess.objects.count() == 1
|
||||
|
||||
|
||||
def test_api_domain_accesses_delete__last_owner_forbidden():
|
||||
def test_api_mail_domain__accesses_delete_owners_last_owner():
|
||||
"""
|
||||
It should not be possible to delete the last owner access from a mail domain
|
||||
"""
|
||||
|
||||
+9
-9
@@ -27,7 +27,8 @@ def test_api_mail_domain__accesses_list_anonymous():
|
||||
|
||||
def test_api_mail_domain__accesses_list_authenticated_unrelated():
|
||||
"""
|
||||
User with no access should not be allowed to list domain accesses.
|
||||
Authenticated users should not be allowed to list mail_domain accesses for a mail_domain
|
||||
to which they are not related.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
mail_domain = factories.MailDomainFactory()
|
||||
@@ -42,9 +43,12 @@ def test_api_mail_domain__accesses_list_authenticated_unrelated():
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"detail": "No MailDomain matches the given query.",
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -174,12 +178,8 @@ def test_api_mail_domain__accesses_list_authenticated_constant_numqueries(
|
||||
# related users :
|
||||
# - query retrieving logged-in user for user_role annotation
|
||||
# - count from pagination
|
||||
|
||||
# - filter on slug and domain on permission (to return 404 instead of 403 when no access)
|
||||
# - count ?
|
||||
|
||||
# - distinct from viewset
|
||||
with django_assert_num_queries(5):
|
||||
with django_assert_num_queries(3):
|
||||
client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
@@ -189,7 +189,7 @@ def test_api_mail_domain__accesses_list_authenticated_constant_numqueries(
|
||||
factories.MailDomainAccessFactory(domain=mail_domain)
|
||||
|
||||
# num queries should still be the same
|
||||
with django_assert_num_queries(5):
|
||||
with django_assert_num_queries(3):
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/accesses/",
|
||||
)
|
||||
|
||||
+4
-2
@@ -43,7 +43,7 @@ def test_api_mail_domain__accesses_retrieve_authenticated_unrelated():
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/accesses/{access.id!s}/",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "No MailDomain matches the given query."}
|
||||
assert response.json() == {"detail": "No MailDomainAccess matches the given query."}
|
||||
|
||||
# Accesses related to another mail_domain should be excluded even if the user is related to it
|
||||
for other_access in [
|
||||
@@ -55,7 +55,9 @@ def test_api_mail_domain__accesses_retrieve_authenticated_unrelated():
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "No MailDomain matches the given query."}
|
||||
assert response.json() == {
|
||||
"detail": "No MailDomainAccess matches the given query."
|
||||
}
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_retrieve_authenticated_related():
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ def test_api_mail_domain__accesses_update_anonymous():
|
||||
assert access.role == enums.MailDomainRoleChoices.VIEWER
|
||||
|
||||
|
||||
def test_api_mail_domain__accesses_update_no_access_forbidden_not_found():
|
||||
def test_api_mail_domain__accesses_update_authenticated_unrelated():
|
||||
"""
|
||||
An authenticated user should not be allowed to update a mail domain access
|
||||
for a mail_domain to which they are not related.
|
||||
@@ -42,7 +42,7 @@ def test_api_mail_domain__accesses_update_no_access_forbidden_not_found():
|
||||
{"role": enums.MailDomainRoleChoices.ADMIN},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
access.refresh_from_db()
|
||||
assert access.role == enums.MailDomainRoleChoices.VIEWER
|
||||
|
||||
|
||||
+4
-2
@@ -30,7 +30,9 @@ def test_api_mail_domain__available_users_anonymous():
|
||||
|
||||
|
||||
def test_api_mail_domain__available_users_forbidden():
|
||||
"""Users with no access should not be able to list available users."""
|
||||
"""Authenticated user without accesses on maildomain should not be able to see available
|
||||
users.
|
||||
"""
|
||||
authenticated_user = core_factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(authenticated_user)
|
||||
@@ -38,7 +40,7 @@ def test_api_mail_domain__available_users_forbidden():
|
||||
|
||||
response = client.get(f"/api/v1.0/mail-domains/{maildomain.slug}/accesses/users/")
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ def test_api_mailboxes__disable_anonymous_forbidden():
|
||||
assert models.Mailbox.objects.get().status == enums.MailboxStatusChoices.ENABLED
|
||||
|
||||
|
||||
def test_api_mailboxes__disable_no_access_forbidden_not_found():
|
||||
def test_api_mailboxes__disable_authenticated_failure():
|
||||
"""Authenticated users should not be able to disable mailbox
|
||||
without specific role on mail domain."""
|
||||
user = core_factories.UserFactory()
|
||||
@@ -39,7 +39,7 @@ def test_api_mailboxes__disable_no_access_forbidden_not_found():
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/disable/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.Mailbox.objects.get().status == enums.MailboxStatusChoices.ENABLED
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ def test_api_mailboxes__enable_anonymous_forbidden():
|
||||
assert models.Mailbox.objects.get().status == enums.MailboxStatusChoices.DISABLED
|
||||
|
||||
|
||||
def test_api_mailboxes__enable_unrelated_not_found():
|
||||
"""Unrelated users should not be able to enable mailbox."""
|
||||
def test_api_mailboxes__enable_authenticated_failure():
|
||||
"""Authenticated users should not be able to enable mailbox
|
||||
without specific role on mail domain."""
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -38,7 +39,7 @@ def test_api_mailboxes__enable_unrelated_not_found():
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/enable/",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert models.Mailbox.objects.get().status == enums.MailboxStatusChoices.DISABLED
|
||||
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ def test_api_mailboxes__list_anonymous():
|
||||
}
|
||||
|
||||
|
||||
def test_api_mailboxes__list_unrelated_not_found():
|
||||
"""Unrelated users should not be able to list mailboxes"""
|
||||
def test_api_mailboxes__list_authenticated():
|
||||
"""Authenticated users should not be able to list mailboxes"""
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
@@ -36,8 +36,10 @@ def test_api_mailboxes__list_unrelated_not_found():
|
||||
factories.MailboxFactory.create_batch(2, domain=mail_domain)
|
||||
|
||||
response = client.get(f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/")
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {"detail": "No MailDomain matches the given query."}
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
"""
|
||||
Unit tests for the mailbox API
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_mailboxes_patch__anonymous_forbidden():
|
||||
"""Anonymous users should not be able to update a mailbox."""
|
||||
mailbox = factories.MailboxFactory()
|
||||
saved_secondary = mailbox.secondary_email
|
||||
|
||||
response = APIClient().patch(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
{"secondary_email": "updated@example.com"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.secondary_email == saved_secondary
|
||||
|
||||
|
||||
def test_api_mailboxes_patch__no_access_forbidden_not_found():
|
||||
"""Authenticated but unauthoriezd users should not be able to update mailboxes."""
|
||||
mailbox = factories.MailboxFactory()
|
||||
saved_secondary = mailbox.secondary_email
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
response = client.patch(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.local_part}/",
|
||||
{"secondary_email": "updated@example.com"},
|
||||
format="json",
|
||||
)
|
||||
# permission denied at domain level
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.secondary_email == saved_secondary
|
||||
|
||||
|
||||
def test_api_mailboxes_patch__viewer_forbidden():
|
||||
"""User having viewer access on a domain should not be able to update
|
||||
anything on a mailbox that's not theirs."""
|
||||
viewer = core_factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
user=viewer,
|
||||
domain=mailbox.domain,
|
||||
role=enums.MailDomainRoleChoices.VIEWER,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(viewer)
|
||||
|
||||
# should not be able to update any field
|
||||
# we only try one field as 403 is global in our implementation
|
||||
old_value = mailbox.secondary_email
|
||||
response = client.patch(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
{"secondary_email": "updated@example.com"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.secondary_email == old_value
|
||||
|
||||
|
||||
def test_api_mailboxes_patch__unauthorized_no_mailbox():
|
||||
"""No mailbox returns a 404."""
|
||||
access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.ADMIN)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.patch(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/nonexistent_mailbox_pk/",
|
||||
{"secondary_email": "updated@example.com"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
# permission denied at domain level
|
||||
# the existence of the mailbox is not checked
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
# UPDATABLE FIELDS : SECONDARY EMAIL FIRST NAME AND LAST NAME
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["owner", "administrator"],
|
||||
)
|
||||
def test_api_mailboxes_patch__admins_can_update_mailboxes(role):
|
||||
"""Domain owners and admins should be allowed to update secondary email on a mailbox"""
|
||||
mailbox = factories.MailboxFactory()
|
||||
user = core_factories.UserFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
user=user,
|
||||
domain=mailbox.domain,
|
||||
role=role,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
valid_new_values = {
|
||||
"secondary_email": "updated_mail@validformat.com",
|
||||
"first_name": "Marsha",
|
||||
"last_name": "Johnson",
|
||||
}
|
||||
for field_name in ["first_name", "last_name", "secondary_email"]:
|
||||
response = client.patch(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
{field_name: valid_new_values[field_name]},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mailbox.refresh_from_db()
|
||||
assert getattr(mailbox, field_name) == valid_new_values[field_name]
|
||||
|
||||
|
||||
def test_api_mailboxes_patch__viewer_can_update_own_mailbox():
|
||||
"""Domain owners and admins should be allowed to update secondary email on a mailbox"""
|
||||
mailbox = factories.MailboxFactory()
|
||||
user = core_factories.UserFactory(email=f"{mailbox.local_part}@{mailbox.domain}")
|
||||
factories.MailDomainAccessFactory(
|
||||
user=user,
|
||||
domain=mailbox.domain,
|
||||
role=enums.MailDomainRoleChoices.VIEWER,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
valid_new_values = {
|
||||
"secondary_email": "updated_mail@validformat.com",
|
||||
"first_name": "Marsha",
|
||||
"last_name": "Johnson",
|
||||
}
|
||||
for field_name in ["first_name", "last_name", "secondary_email"]:
|
||||
response = client.patch(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
{field_name: valid_new_values[field_name]},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mailbox.refresh_from_db()
|
||||
assert getattr(mailbox, field_name) == valid_new_values[field_name]
|
||||
|
||||
|
||||
# DOMAIN AND LOCAL PART
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["viewer", "owner", "administrator"],
|
||||
)
|
||||
def test_api_mailboxes_patch__no_updating_domain_or_local_part(role):
|
||||
"""Domain and local parts should not be updated."""
|
||||
mailbox = factories.MailboxFactory()
|
||||
user = core_factories.UserFactory()
|
||||
if role == "viewer":
|
||||
# viewer has similar privileges as admins on own mailbox
|
||||
user = core_factories.UserFactory(
|
||||
email=f"{mailbox.local_part}@{mailbox.domain}"
|
||||
)
|
||||
access = factories.MailDomainAccessFactory(
|
||||
user=user,
|
||||
domain=mailbox.domain,
|
||||
role=role,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_domain = factories.MailDomainEnabledFactory()
|
||||
local_part = mailbox.local_part
|
||||
response = client.patch(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
{"local_part": "new.local.part", "domain": other_domain.name},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK # a 400 would be better
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.local_part == local_part
|
||||
assert mailbox.domain == access.domain
|
||||
@@ -1,192 +0,0 @@
|
||||
"""
|
||||
Unit tests for the mailbox API
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
# PUT expects all the object's updatable fields.
|
||||
# We'll use this dict on all the following tests
|
||||
NEW_VALUES = {
|
||||
"secondary_email": "marsha.p@social.us",
|
||||
"first_name": "Marsha",
|
||||
"last_name": "Johnson",
|
||||
}
|
||||
|
||||
|
||||
def test_api_mailboxes_put__anonymous_forbidden():
|
||||
"""Anonymous users should not be able to update a mailbox."""
|
||||
mailbox = factories.MailboxFactory()
|
||||
saved_secondary = mailbox.secondary_email
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
NEW_VALUES,
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.secondary_email == saved_secondary
|
||||
|
||||
|
||||
def test_api_mailboxes_put__no_access_forbidden_not_found():
|
||||
"""Authenticated but unauthoriezd users should not be able to update mailboxes."""
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
|
||||
mailbox = factories.MailboxFactory()
|
||||
saved_secondary = mailbox.secondary_email
|
||||
response = client.put(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
NEW_VALUES,
|
||||
format="json",
|
||||
)
|
||||
|
||||
# permission denied at domain level
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.secondary_email == saved_secondary
|
||||
|
||||
|
||||
def test_api_mailboxes_put__unauthorized_no_mailbox():
|
||||
"""Authenticated but unauthoriezd users should not be able to update mailboxes."""
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
response = client.put(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/mailboxes/nonexistent_mailbox_pk/",
|
||||
NEW_VALUES,
|
||||
format="json",
|
||||
)
|
||||
|
||||
# permission denied at domain level
|
||||
# the existence of the mailbox is not checked
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
def test_api_mailboxes_put__viewer_forbidden():
|
||||
"""User having viewer access on a domain should not be able to update
|
||||
anything on a mailbox that's not theirs."""
|
||||
viewer = core_factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
user=viewer,
|
||||
domain=mailbox.domain,
|
||||
role=enums.MailDomainRoleChoices.VIEWER,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(viewer)
|
||||
|
||||
# should not be able to update any field
|
||||
# we only try one field as 403 is global in our implementation
|
||||
old_value = mailbox.secondary_email
|
||||
response = client.put(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
NEW_VALUES,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.secondary_email == old_value
|
||||
|
||||
|
||||
# UPDATABLE FIELDS : SECONDARY EMAIL, FIRST NAME AND LAST NAME
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["owner", "administrator"],
|
||||
)
|
||||
def test_api_mailboxes_put__admins_can_update_mailboxes(role):
|
||||
"""Domain owners and admins should be allowed to update secondary email on a mailbox"""
|
||||
mailbox = factories.MailboxFactory()
|
||||
user = core_factories.UserFactory()
|
||||
factories.MailDomainAccessFactory(
|
||||
user=user,
|
||||
domain=mailbox.domain,
|
||||
role=role,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
NEW_VALUES,
|
||||
format="json",
|
||||
)
|
||||
result = response.json()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mailbox.refresh_from_db()
|
||||
assert result["first_name"] == NEW_VALUES["first_name"]
|
||||
assert result["last_name"] == NEW_VALUES["last_name"]
|
||||
assert result["secondary_email"] == NEW_VALUES["secondary_email"]
|
||||
|
||||
|
||||
def test_api_mailboxes_put__viewer_can_update_own_mailbox():
|
||||
"""Domain owners and admins should be allowed to update secondary email on a mailbox"""
|
||||
mailbox = factories.MailboxFactory()
|
||||
user = core_factories.UserFactory(email=f"{mailbox.local_part}@{mailbox.domain}")
|
||||
factories.MailDomainAccessFactory(
|
||||
user=user,
|
||||
domain=mailbox.domain,
|
||||
role=enums.MailDomainRoleChoices.VIEWER,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
NEW_VALUES,
|
||||
format="json",
|
||||
)
|
||||
result = response.json()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
mailbox.refresh_from_db()
|
||||
assert result["first_name"] == NEW_VALUES["first_name"]
|
||||
assert result["last_name"] == NEW_VALUES["last_name"]
|
||||
assert result["secondary_email"] == NEW_VALUES["secondary_email"]
|
||||
|
||||
|
||||
# DOMAIN AND LOCAL PART
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["viewer", "owner", "administrator"],
|
||||
)
|
||||
def test_api_mailboxes_put__no_updating_domain_or_local_part(role):
|
||||
"""Domain and local parts should not be updated."""
|
||||
mailbox = factories.MailboxFactory()
|
||||
user = core_factories.UserFactory()
|
||||
if role == "viewer":
|
||||
# viewer has similar privileges as admins on own mailbox
|
||||
user = core_factories.UserFactory(
|
||||
email=f"{mailbox.local_part}@{mailbox.domain}"
|
||||
)
|
||||
access = factories.MailDomainAccessFactory(
|
||||
user=user,
|
||||
domain=mailbox.domain,
|
||||
role=role,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
other_domain = factories.MailDomainEnabledFactory()
|
||||
old_local_part = mailbox.local_part
|
||||
response = client.put(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/",
|
||||
{**NEW_VALUES, "local_part": "new.local.part", "domain": other_domain.name},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK # This should not be a 200
|
||||
mailbox.refresh_from_db()
|
||||
assert mailbox.local_part == old_local_part
|
||||
assert mailbox.domain == access.domain
|
||||
+17
-7
@@ -28,7 +28,7 @@ def test_api_mailboxes__reset_password_anonymous_unauthorized():
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_mailboxes__reset_password_no_access_forbidden_not_found():
|
||||
def test_api_mailboxes__reset_password_unrelated_forbidden():
|
||||
"""Authenticated users not managing the domain
|
||||
should not be able to reset its mailboxes password."""
|
||||
user = core_factories.UserFactory()
|
||||
@@ -41,9 +41,9 @@ def test_api_mailboxes__reset_password_no_access_forbidden_not_found():
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/reset_password/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {
|
||||
"detail": "No MailDomain matches the given query.",
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ Please add a valid secondary email before trying again."
|
||||
],
|
||||
)
|
||||
@responses.activate
|
||||
def test_api_mailboxes__reset_password_admin_successful(role, dimail_token_ok): # pylint: disable=W0613
|
||||
def test_api_mailboxes__reset_password_admin_successful(role):
|
||||
"""Owner and admin users should be able to reset password on mailboxes.
|
||||
New password should be sent to secondary email."""
|
||||
mail_domain = factories.MailDomainEnabledFactory()
|
||||
@@ -124,7 +124,12 @@ def test_api_mailboxes__reset_password_admin_successful(role, dimail_token_ok):
|
||||
client.force_login(access.user)
|
||||
dimail_url = settings.MAIL_PROVISIONING_API_URL
|
||||
|
||||
# token response in fixtures
|
||||
responses.add(
|
||||
responses.GET,
|
||||
f"{dimail_url}/token/",
|
||||
body=dimail.TOKEN_OK,
|
||||
status=200,
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
f"{dimail_url}/domains/{mail_domain.name}/mailboxes/{mailbox.local_part}/reset_password/",
|
||||
@@ -156,7 +161,7 @@ def test_api_mailboxes__reset_password_non_existing():
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_mailboxes__reset_password_connexion_failed(dimail_token_ok): # pylint: disable=W0613
|
||||
def test_api_mailboxes__reset_password_connexion_failed():
|
||||
"""
|
||||
No mail is sent when password reset failed because of connexion error.
|
||||
"""
|
||||
@@ -170,7 +175,12 @@ def test_api_mailboxes__reset_password_connexion_failed(dimail_token_ok): # pyl
|
||||
client.force_login(access.user)
|
||||
|
||||
dimail_url = settings.MAIL_PROVISIONING_API_URL
|
||||
# token response in fixtures
|
||||
responses.add(
|
||||
responses.GET,
|
||||
f"{dimail_url}/token/",
|
||||
body=dimail.TOKEN_OK,
|
||||
status=200,
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
f"{dimail_url}/domains/{mail_domain.name}/mailboxes/{mailbox.local_part}/reset_password/",
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
"""
|
||||
Unit tests for the mailbox API
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories as core_factories
|
||||
|
||||
from mailbox_manager import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_mailboxes__retrieve_anonymous_forbidden():
|
||||
"""Anonymous users should not be able to retrieve a new mailbox via the API."""
|
||||
mailbox = factories.MailboxFactory()
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/"
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_api_mailboxes__retrieve_no_access_forbidden_not_found():
|
||||
"""Authenticated but unauthorized users should not be able to
|
||||
retrieve mailbox. Response should be indistinguisable from normal 404"""
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory())
|
||||
|
||||
mailbox = factories.MailboxFactory()
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
# even if they have a mailbox on this domain
|
||||
mailbox = factories.MailboxFactory()
|
||||
client.force_login(core_factories.UserFactory(email=str(mailbox)))
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
def test_api_mailboxes__retrieve_authorized_ok():
|
||||
"""Authorized users should be able to retrieve mailboxes."""
|
||||
|
||||
access = factories.MailDomainAccessFactory()
|
||||
mailbox = factories.MailboxFactory(domain=access.domain)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
response = client.get(
|
||||
f"/api/v1.0/mail-domains/{mailbox.domain.slug}/mailboxes/{mailbox.pk}/"
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(mailbox.id),
|
||||
"first_name": mailbox.first_name,
|
||||
"last_name": mailbox.last_name,
|
||||
"local_part": mailbox.local_part,
|
||||
"secondary_email": mailbox.secondary_email,
|
||||
"status": mailbox.status,
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"""
|
||||
Fixtures for mailbox manager tests
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
|
||||
from mailbox_manager import factories
|
||||
|
||||
|
||||
## DIMAIL RESPONSES
|
||||
@pytest.fixture(name="dimail_token_ok")
|
||||
def fixture_dimail_token_ok():
|
||||
"""Mock dimail response when /token/ endpoit is given valid credentials."""
|
||||
responses.get(
|
||||
re.compile(r".*/token/"),
|
||||
json={"access_token": "token", "token_type": "bearer"},
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="mailbox_data")
|
||||
def fixture_mailbox_data():
|
||||
"""Provides valid mailbox data for tests."""
|
||||
example = factories.MailboxFactory.build()
|
||||
return {
|
||||
"first_name": example.first_name,
|
||||
"last_name": example.last_name,
|
||||
"local_part": example.local_part,
|
||||
"secondary_email": example.secondary_email,
|
||||
}
|
||||
+12
-2
@@ -3,8 +3,9 @@
|
||||
|
||||
import json
|
||||
|
||||
|
||||
## USERS
|
||||
|
||||
|
||||
def response_user_created(user_sub):
|
||||
"""mimic dimail response upon successful user creation."""
|
||||
return json.dumps(
|
||||
@@ -18,6 +19,7 @@ def response_user_created(user_sub):
|
||||
|
||||
|
||||
## DOMAINS
|
||||
|
||||
CHECK_DOMAIN_BROKEN = {
|
||||
"name": "example.fr",
|
||||
"state": "broken",
|
||||
@@ -287,7 +289,13 @@ DOMAIN_SPEC = [
|
||||
]
|
||||
|
||||
|
||||
## TOKEN
|
||||
|
||||
TOKEN_OK = json.dumps({"access_token": "token", "token_type": "bearer"})
|
||||
|
||||
## ALLOWS
|
||||
|
||||
|
||||
def response_allows_created(user_name, domain_name):
|
||||
"""mimic dimail response upon successful allows creation.
|
||||
Dimail expects a name but our names are ProConnect's uuids."""
|
||||
@@ -295,6 +303,8 @@ def response_allows_created(user_name, domain_name):
|
||||
|
||||
|
||||
## MAILBOXES
|
||||
|
||||
|
||||
def response_mailbox_created(email_address):
|
||||
"""mimic dimail response upon successful mailbox creation."""
|
||||
return json.dumps({"email": email_address.lower(), "password": "password"})
|
||||
return json.dumps({"email": email_address, "password": "password"})
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"""
|
||||
Unit tests for the Alias model
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from mailbox_manager import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_models_aliases__devnull_destination_ok():
|
||||
"""Can create alias where destination is devnull@devnull."""
|
||||
|
||||
models.Alias.objects.create(
|
||||
local_part="spam",
|
||||
domain=factories.MailDomainEnabledFactory(),
|
||||
destination="devnull@devnull",
|
||||
)
|
||||
@@ -19,6 +19,7 @@ from .fixtures.dimail import (
|
||||
CHECK_DOMAIN_BROKEN,
|
||||
CHECK_DOMAIN_OK,
|
||||
DOMAIN_SPEC,
|
||||
TOKEN_OK,
|
||||
response_mailbox_created,
|
||||
)
|
||||
|
||||
@@ -100,9 +101,7 @@ def test_fetch_domain_status__should_switch_to_failed_when_domain_broken(client)
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.django_db
|
||||
def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(
|
||||
client, dimail_token_ok
|
||||
): # pylint: disable=W0613
|
||||
def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(client):
|
||||
"""Test admin action should switch domain state to ENABLED
|
||||
when dimail's response is "ok". It should also activate any pending mailbox."""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
@@ -128,7 +127,13 @@ def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(
|
||||
content_type="application/json",
|
||||
)
|
||||
# we need to get a token to create mailboxes
|
||||
# token response in fixtures
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(rf".*/domains/{domain1.name}/mailboxes/"),
|
||||
@@ -203,7 +208,7 @@ def test_fetch_domain_expected_config__should_not_fetch_for_disabled_domain(clie
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.django_db
|
||||
def test_send_pending_mailboxes(client, dimail_token_ok): # pylint: disable=W0613
|
||||
def test_send_pending_mailboxes(client):
|
||||
"""Test admin action to send pending mailboxes to dimail."""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
client.force_login(admin)
|
||||
@@ -218,7 +223,13 @@ def test_send_pending_mailboxes(client, dimail_token_ok): # pylint: disable=W06
|
||||
|
||||
url = reverse("admin:mailbox_manager_maildomain_changelist")
|
||||
for mailbox in mailboxes:
|
||||
# token response in fixtures
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
@@ -236,7 +247,7 @@ def test_send_pending_mailboxes(client, dimail_token_ok): # pylint: disable=W06
|
||||
|
||||
@responses.activate
|
||||
@pytest.mark.django_db
|
||||
def test_send_pending_mailboxes__listing_failed_mailboxes(client, dimail_token_ok): # pylint: disable=W0613
|
||||
def test_send_pending_mailboxes__listing_failed_mailboxes(client):
|
||||
"""Test admin action to send pending mailboxes to dimail."""
|
||||
admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
|
||||
client.force_login(admin)
|
||||
@@ -250,7 +261,13 @@ def test_send_pending_mailboxes__listing_failed_mailboxes(client, dimail_token_o
|
||||
}
|
||||
|
||||
url = reverse("admin:mailbox_manager_maildomain_changelist")
|
||||
# token response in fixtures
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
responses.add(
|
||||
responses.POST,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
Unit tests for dimail client
|
||||
"""
|
||||
|
||||
# pylint: disable=W0613
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from email.errors import HeaderParseError, NonASCIILocalPartDefect
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
@@ -19,14 +21,14 @@ from .fixtures.dimail import (
|
||||
CHECK_DOMAIN_BROKEN_EXTERNAL,
|
||||
CHECK_DOMAIN_BROKEN_INTERNAL,
|
||||
CHECK_DOMAIN_OK,
|
||||
TOKEN_OK,
|
||||
response_mailbox_created,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_dimail_synchronization__already_sync(dimail_token_ok):
|
||||
def test_dimail_synchronization__already_sync():
|
||||
"""
|
||||
No mailbox should be created when everything is already synced.
|
||||
"""
|
||||
@@ -37,26 +39,35 @@ def test_dimail_synchronization__already_sync(dimail_token_ok):
|
||||
assert pre_sync_mailboxes.count() == 3
|
||||
|
||||
dimail_client = DimailAPIClient()
|
||||
|
||||
# Ensure successful response using "responses":
|
||||
# token response in fixtures
|
||||
responses.get(
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
json=[
|
||||
{
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"{mailbox.local_part}@{domain.name}",
|
||||
"givenName": mailbox.first_name,
|
||||
"surName": mailbox.last_name,
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
}
|
||||
for mailbox in pre_sync_mailboxes
|
||||
],
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
imported_mailboxes = dimail_client.import_mailboxes(domain)
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response using "responses":
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "dimail_people_token"}',
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
[
|
||||
{
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"{mailbox.local_part}@{domain.name}",
|
||||
"givenName": mailbox.first_name,
|
||||
"surName": mailbox.last_name,
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
}
|
||||
for mailbox in pre_sync_mailboxes
|
||||
]
|
||||
),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
imported_mailboxes = dimail_client.import_mailboxes(domain)
|
||||
|
||||
post_sync_mailboxes = models.Mailbox.objects.filter(domain=domain)
|
||||
assert post_sync_mailboxes.count() == 3
|
||||
@@ -64,159 +75,97 @@ def test_dimail_synchronization__already_sync(dimail_token_ok):
|
||||
assert set(models.Mailbox.objects.filter(domain=domain)) == set(pre_sync_mailboxes)
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_dimail_synchronization__synchronize_mailboxes(caplog, dimail_token_ok): # pylint: disable=W0613, R0914
|
||||
"""Importing mailboxes from dimail should synchronize valid mailboxes
|
||||
and log errors for invalid ones."""
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
@mock.patch.object(Logger, "warning")
|
||||
def test_dimail_synchronization__synchronize_mailboxes(mock_warning):
|
||||
"""A mailbox existing solely on dimail should be synchronized
|
||||
upon calling sync function on its domain"""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
|
||||
existing_alias = factories.AliasFactory(domain=domain)
|
||||
assert not models.Mailbox.objects.exists()
|
||||
|
||||
dimail_client = DimailAPIClient()
|
||||
# Ensure successful response using "responses":
|
||||
# successful token in fixtures
|
||||
mailbox_valid = {
|
||||
"type": "mailbox",
|
||||
"status": "ok",
|
||||
"email": f"validmailbox@{domain.name}",
|
||||
"givenName": "Michael",
|
||||
"surName": "Roch",
|
||||
"displayName": "Michael Roch",
|
||||
}
|
||||
mailbox_oxadmin = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"oxadmin@{domain.name}",
|
||||
"givenName": "Admin",
|
||||
"surName": "Context",
|
||||
"displayName": "Context Admin",
|
||||
}
|
||||
mailbox_with_wrong_domain = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": "johndoe@wrongdomain.com",
|
||||
"givenName": "John",
|
||||
"surName": "Doe",
|
||||
"displayName": "John Doe",
|
||||
}
|
||||
mailbox_with_invalid_domain = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"naw@ake@{domain.name}",
|
||||
"givenName": "Joe",
|
||||
"surName": "Doe",
|
||||
"displayName": "Joe Doe",
|
||||
}
|
||||
mailbox_with_invalid_local_part = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"obalmaské@{domain.name}",
|
||||
"givenName": "Jean",
|
||||
"surName": "Vang",
|
||||
"displayName": "Jean Vang",
|
||||
}
|
||||
mailbox_existing_alias = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"{existing_alias.local_part}@{domain.name}",
|
||||
"givenName": "Support",
|
||||
"surName": "email",
|
||||
"displayName": "Support email",
|
||||
}
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response using "responses":
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "dimail_people_token"}',
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
responses.get(
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
json=[
|
||||
mailbox_valid,
|
||||
mailbox_oxadmin,
|
||||
mailbox_with_wrong_domain,
|
||||
mailbox_with_invalid_domain,
|
||||
mailbox_with_invalid_local_part,
|
||||
mailbox_existing_alias,
|
||||
],
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
mailbox_valid = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"oxadmin@{domain.name}",
|
||||
"givenName": "Admin",
|
||||
"surName": "Context",
|
||||
"displayName": "Context Admin",
|
||||
}
|
||||
mailbox_with_wrong_domain = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": "johndoe@wrongdomain.com",
|
||||
"givenName": "John",
|
||||
"surName": "Doe",
|
||||
"displayName": "John Doe",
|
||||
}
|
||||
mailbox_with_invalid_domain = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"naw@ake@{domain.name}",
|
||||
"givenName": "Joe",
|
||||
"surName": "Doe",
|
||||
"displayName": "Joe Doe",
|
||||
}
|
||||
mailbox_with_invalid_local_part = {
|
||||
"type": "mailbox",
|
||||
"status": "broken",
|
||||
"email": f"obalmaské@{domain.name}",
|
||||
"givenName": "Jean",
|
||||
"surName": "Vang",
|
||||
"displayName": "Jean Vang",
|
||||
}
|
||||
|
||||
imported_mailboxes = dimail_client.import_mailboxes(domain)
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
[
|
||||
mailbox_valid,
|
||||
mailbox_with_wrong_domain,
|
||||
mailbox_with_invalid_domain,
|
||||
mailbox_with_invalid_local_part,
|
||||
]
|
||||
),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
# 4 imports failed: oxadmin, wrong domain, HeaderParseError, NonASCIILocalPartDefect
|
||||
assert len(caplog.records) == 5
|
||||
log_messages = [record.message for record in caplog.records]
|
||||
imported_mailboxes = dimail_client.import_mailboxes(domain)
|
||||
|
||||
expected_messages = [
|
||||
f"Not importing OX technical address: oxadmin@{domain.name}",
|
||||
f"Import of email {mailbox_with_wrong_domain['email']} failed because of a wrong domain",
|
||||
f"Import of email {mailbox_with_invalid_domain['email']} failed with error Invalid Domain",
|
||||
f"Import of email {mailbox_with_invalid_local_part['email']} failed with error local-part \
|
||||
contains non-ASCII characters)",
|
||||
]
|
||||
for message in expected_messages:
|
||||
assert message in log_messages
|
||||
# 3 imports failed: wrong domain, HeaderParseError, NonASCIILocalPartDefect
|
||||
assert mock_warning.call_count == 3
|
||||
|
||||
assert models.Mailbox.objects.count() == 2
|
||||
assert imported_mailboxes == [
|
||||
mailbox_valid["email"],
|
||||
mailbox_existing_alias["email"],
|
||||
]
|
||||
# first we try to import email with a wrong domain
|
||||
assert mock_warning.call_args_list[0][0] == (
|
||||
"Import of email %s failed because of a wrong domain",
|
||||
mailbox_with_wrong_domain["email"],
|
||||
)
|
||||
|
||||
# then we try to import email with invalid domain
|
||||
invalid_mailbox_log = mock_warning.call_args_list[1][0]
|
||||
assert invalid_mailbox_log[1] == mailbox_with_invalid_domain["email"]
|
||||
assert isinstance(invalid_mailbox_log[2], HeaderParseError)
|
||||
|
||||
@responses.activate
|
||||
def test_dimail_synchronization__synchronize_aliases(dimail_token_ok): # pylint: disable=unused-argument
|
||||
"""Importing aliases from dimail should synchronize valid aliases
|
||||
and log errors for invalid ones."""
|
||||
alias = factories.AliasFactory()
|
||||
dimail_client = DimailAPIClient()
|
||||
# finally we try to import email with non ascii local part
|
||||
non_ascii_mailbox_log = mock_warning.call_args_list[2][0]
|
||||
assert non_ascii_mailbox_log[1] == mailbox_with_invalid_local_part["email"]
|
||||
assert isinstance(non_ascii_mailbox_log[2], NonASCIILocalPartDefect)
|
||||
|
||||
existing_mailbox = factories.MailboxFactory(domain=alias.domain)
|
||||
|
||||
# Ensure successful response using "responses":
|
||||
# token response in fixtures
|
||||
incoming_aliases = [
|
||||
{
|
||||
"username": "contact",
|
||||
"domain": alias.domain.name,
|
||||
"destination": alias.destination, # same destination = ok
|
||||
"allow_to_send": False,
|
||||
},
|
||||
{
|
||||
"username": alias.local_part, # same username = ok
|
||||
"domain": alias.domain.name,
|
||||
"destination": "maheius.endorecles@somethingelse.com",
|
||||
"allow_to_send": False,
|
||||
},
|
||||
{ # same username + same destination = big nono
|
||||
"username": alias.local_part,
|
||||
"domain": alias.domain.name,
|
||||
"destination": alias.destination,
|
||||
"allow_to_send": False,
|
||||
},
|
||||
{ # mailbox with same username = ok
|
||||
"username": existing_mailbox.local_part,
|
||||
"domain": alias.domain.name,
|
||||
"destination": existing_mailbox.secondary_email,
|
||||
"allow_to_send": False,
|
||||
},
|
||||
{ # alias to devnull@devnull
|
||||
"username": "spam",
|
||||
"domain": alias.domain.name,
|
||||
"destination": "devnull@devnull",
|
||||
"allow_to_send": False,
|
||||
},
|
||||
]
|
||||
responses.get(
|
||||
re.compile(rf".*/domains/{alias.domain.name}/aliases/"),
|
||||
json=incoming_aliases,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
imported_aliases = dimail_client.import_aliases(alias.domain)
|
||||
|
||||
assert len(imported_aliases) == 4
|
||||
assert models.Alias.objects.count() == 5
|
||||
mailbox = models.Mailbox.objects.get()
|
||||
assert mailbox.local_part == "oxadmin"
|
||||
assert mailbox.status == enums.MailboxStatusChoices.ENABLED
|
||||
assert imported_mailboxes == [mailbox_valid["email"]]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -234,9 +183,10 @@ def test_dimail__fetch_domain_status__switch_to_enabled(domain_status):
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
body_content = CHECK_DOMAIN_OK.copy()
|
||||
body_content["name"] = domain.name
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
json=body_content,
|
||||
body=json.dumps(body_content),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -271,9 +221,10 @@ def test_dimail__fetch_domain_status__switch_to_action_required(
|
||||
domain = factories.MailDomainFactory(status=domain_status)
|
||||
body_domain_broken = CHECK_DOMAIN_BROKEN_EXTERNAL.copy()
|
||||
body_domain_broken["name"] = domain.name
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
json=body_domain_broken,
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -287,9 +238,10 @@ def test_dimail__fetch_domain_status__switch_to_action_required(
|
||||
# Now domain is OK again
|
||||
body_domain_ok = CHECK_DOMAIN_OK.copy()
|
||||
body_domain_ok["name"] = domain.name
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
json=body_domain_ok,
|
||||
body=json.dumps(body_domain_ok),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -315,16 +267,18 @@ def test_dimail__fetch_domain_status__switch_to_failed(domain_status):
|
||||
# nothing can be done by support team, domain should be in failed
|
||||
body_domain_broken = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_domain_broken["name"] = domain.name
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
json=body_domain_broken,
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
# the endpoint fix is called and still returns KO for internal checks
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/fix/"),
|
||||
json=body_domain_broken,
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -351,9 +305,10 @@ def test_dimail__fetch_domain_status__full_fix_scenario(domain_status):
|
||||
# with all checks KO, domain should be in action required
|
||||
body_domain_broken = CHECK_DOMAIN_BROKEN.copy()
|
||||
body_domain_broken["name"] = domain.name
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
json=body_domain_broken,
|
||||
body=json.dumps(body_domain_broken),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -369,18 +324,20 @@ def test_dimail__fetch_domain_status__full_fix_scenario(domain_status):
|
||||
# the fetch_domain_status call
|
||||
body_domain_broken_internal = CHECK_DOMAIN_BROKEN_INTERNAL.copy()
|
||||
body_domain_broken_internal["name"] = domain.name
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/check/"),
|
||||
json=body_domain_broken_internal,
|
||||
body=json.dumps(body_domain_broken_internal),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
# the endpoint fix is called and returns OK. Hooray!
|
||||
body_domain_ok = CHECK_DOMAIN_OK.copy()
|
||||
body_domain_ok["name"] = domain.name
|
||||
responses.get(
|
||||
responses.add(
|
||||
responses.GET,
|
||||
re.compile(rf".*/domains/{domain.name}/fix/"),
|
||||
json=body_domain_ok,
|
||||
body=json.dumps(body_domain_ok),
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
@@ -391,8 +348,7 @@ def test_dimail__fetch_domain_status__full_fix_scenario(domain_status):
|
||||
assert domain.last_check_details == body_domain_ok
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_dimail__send_pending_mailboxes(caplog, dimail_token_ok):
|
||||
def test_dimail__send_pending_mailboxes(caplog):
|
||||
"""Status of pending mailboxes should switch to "enabled"
|
||||
when calling send_pending_mailboxes."""
|
||||
caplog.set_level(logging.INFO)
|
||||
@@ -409,16 +365,22 @@ def test_dimail__send_pending_mailboxes(caplog, dimail_token_ok):
|
||||
)
|
||||
|
||||
dimail_client = DimailAPIClient()
|
||||
|
||||
# Ensure successful response using "responses":
|
||||
# token response in fixtures
|
||||
responses.post(
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
body=response_mailbox_created(f"mock@{domain.name}"),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client.send_pending_mailboxes(domain=domain)
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body=TOKEN_OK,
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
body=response_mailbox_created(f"mock@{domain.name}"),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
dimail_client.send_pending_mailboxes(domain=domain)
|
||||
|
||||
mailbox1.refresh_from_db()
|
||||
mailbox2.refresh_from_db()
|
||||
|
||||
@@ -29,11 +29,7 @@ maildomain_related_router.register(
|
||||
viewsets.MailDomainInvitationViewset,
|
||||
basename="invitations",
|
||||
)
|
||||
maildomain_related_router.register(
|
||||
"aliases",
|
||||
viewsets.AliasViewSet,
|
||||
basename="aliases",
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"""A minimalist client to synchronize with mailbox provisioning API."""
|
||||
|
||||
import ast
|
||||
import json
|
||||
import smtplib
|
||||
from email.errors import HeaderParseError, NonASCIILocalPartDefect
|
||||
@@ -45,7 +46,7 @@ class DimailAPIClient:
|
||||
API_CREDENTIALS = settings.MAIL_PROVISIONING_API_CREDENTIALS
|
||||
API_TIMEOUT = settings.MAIL_PROVISIONING_API_TIMEOUT
|
||||
|
||||
def _get_headers(self):
|
||||
def get_headers(self):
|
||||
"""
|
||||
Return Bearer token. Requires MAIL_PROVISIONING_API_CREDENTIALS setting,
|
||||
to get a token from dimail /token/ endpoint.
|
||||
@@ -82,7 +83,7 @@ class DimailAPIClient:
|
||||
"Token denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
|
||||
)
|
||||
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def create_domain(self, domain_name, request_user):
|
||||
"""Send a domain creation request to dimail API."""
|
||||
@@ -117,7 +118,7 @@ class DimailAPIClient:
|
||||
)
|
||||
return response
|
||||
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def create_mailbox(self, mailbox, request_user=None):
|
||||
"""Send a CREATE mailbox request to mail provisioning API."""
|
||||
@@ -130,7 +131,7 @@ class DimailAPIClient:
|
||||
# displayName value has to be unique
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
}
|
||||
headers = self._get_headers()
|
||||
headers = self.get_headers()
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
@@ -165,36 +166,7 @@ class DimailAPIClient:
|
||||
"Permission denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
|
||||
)
|
||||
|
||||
# Dimail doesn't return a clear error for duplicate yet
|
||||
# but a 500 internal error
|
||||
if response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
|
||||
try:
|
||||
address = session.get(
|
||||
f"{self.API_URL}/domains/{mailbox.domain.name}/address/{mailbox.local_part}/",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logger.error(
|
||||
"Connection error while trying to reach %s.",
|
||||
self.API_URL,
|
||||
exc_info=error,
|
||||
)
|
||||
raise error
|
||||
|
||||
if address.status_code == status.HTTP_200_OK:
|
||||
primary = address.json()["ox_primary_email"]
|
||||
raise exceptions.ValidationError(
|
||||
{
|
||||
"NON_FIELD_ERRORS": [
|
||||
f"First name + last name combination already in use in this context : {primary}."
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def create_user(self, user_id):
|
||||
"""Send a request to dimail, to create a new user there. In dimail, user ids are subs."""
|
||||
@@ -231,7 +203,7 @@ class DimailAPIClient:
|
||||
)
|
||||
return response
|
||||
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def create_allow(self, user_id, domain_name):
|
||||
"""Send a request to dimail for a new 'allow' between user and the domain."""
|
||||
@@ -273,9 +245,9 @@ class DimailAPIClient:
|
||||
)
|
||||
return response
|
||||
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def _raise_exception_for_unexpected_response(self, response):
|
||||
def raise_exception_for_unexpected_response(self, response):
|
||||
"""Raise error when encountering an unexpected error in dimail."""
|
||||
try:
|
||||
error_content = json.loads(
|
||||
@@ -299,7 +271,7 @@ class DimailAPIClient:
|
||||
title, template_name, recipient, mailbox_data, issuer
|
||||
)
|
||||
|
||||
def _notify_mailbox_password_reset(self, recipient, mailbox_data, issuer=None):
|
||||
def notify_mailbox_password_reset(self, recipient, mailbox_data, issuer=None):
|
||||
"""
|
||||
Send email to notify of password reset
|
||||
and send new password.
|
||||
@@ -359,7 +331,7 @@ class DimailAPIClient:
|
||||
try:
|
||||
response = session.get(
|
||||
f"{self.API_URL}/domains/{domain.name}/mailboxes/",
|
||||
headers=self._get_headers(),
|
||||
headers=self.get_headers(),
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
@@ -372,48 +344,43 @@ class DimailAPIClient:
|
||||
raise error
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
dimail_mailboxes = ast.literal_eval(
|
||||
response.content.decode("utf-8")
|
||||
) # format output str to proper list
|
||||
|
||||
dimail_mailboxes = response.json()
|
||||
people_mailboxes = models.Mailbox.objects.filter(domain=domain)
|
||||
imported_mailboxes = []
|
||||
for dimail_mailbox in dimail_mailboxes:
|
||||
try:
|
||||
address = Address(addr_spec=dimail_mailbox["email"])
|
||||
except (HeaderParseError, NonASCIILocalPartDefect) as error:
|
||||
logger.warning(
|
||||
"Import of email %s failed with error %s",
|
||||
dimail_mailbox["email"],
|
||||
error,
|
||||
)
|
||||
continue
|
||||
|
||||
if address.username == "oxadmin":
|
||||
logger.warning(
|
||||
"Not importing OX technical address: %s", dimail_mailbox["email"]
|
||||
)
|
||||
continue
|
||||
|
||||
if str(address) not in [
|
||||
if not dimail_mailbox["email"] in [
|
||||
str(people_mailbox) for people_mailbox in people_mailboxes
|
||||
]:
|
||||
# sometimes dimail api returns email from another domain,
|
||||
# so we decide to exclude this kind of email
|
||||
if address.domain == domain.name:
|
||||
# creates a mailbox on our end
|
||||
mailbox = models.Mailbox.objects.create(
|
||||
first_name=dimail_mailbox["givenName"],
|
||||
last_name=dimail_mailbox["surName"],
|
||||
local_part=address.username,
|
||||
domain=domain,
|
||||
status=enums.MailboxStatusChoices.ENABLED,
|
||||
password=make_password(None), # unusable password
|
||||
)
|
||||
imported_mailboxes.append(str(mailbox))
|
||||
else:
|
||||
try:
|
||||
# sometimes dimail api returns email from another domain,
|
||||
# so we decide to exclude this kind of email
|
||||
address = Address(addr_spec=dimail_mailbox["email"])
|
||||
if address.domain == domain.name:
|
||||
# creates a mailbox on our end
|
||||
mailbox = models.Mailbox.objects.create(
|
||||
first_name=dimail_mailbox["givenName"],
|
||||
last_name=dimail_mailbox["surName"],
|
||||
local_part=address.username,
|
||||
domain=domain,
|
||||
status=enums.MailboxStatusChoices.ENABLED,
|
||||
password=make_password(None), # unusable password
|
||||
)
|
||||
imported_mailboxes.append(str(mailbox))
|
||||
else:
|
||||
logger.warning(
|
||||
"Import of email %s failed because of a wrong domain",
|
||||
dimail_mailbox["email"],
|
||||
)
|
||||
except (HeaderParseError, NonASCIILocalPartDefect) as err:
|
||||
logger.warning(
|
||||
"Import of email %s failed because of a wrong domain",
|
||||
"Import of email %s failed with error %s",
|
||||
dimail_mailbox["email"],
|
||||
err,
|
||||
)
|
||||
return imported_mailboxes
|
||||
|
||||
@@ -422,7 +389,7 @@ class DimailAPIClient:
|
||||
response = session.patch(
|
||||
f"{self.API_URL}/domains/{mailbox.domain.name}/mailboxes/{mailbox.local_part}",
|
||||
json={"active": "no"},
|
||||
headers=self._get_headers(),
|
||||
headers=self.get_headers(),
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
@@ -434,7 +401,7 @@ class DimailAPIClient:
|
||||
request_user,
|
||||
)
|
||||
return response
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def enable_mailbox(self, mailbox, request_user=None):
|
||||
"""Send a request to enable a mailbox to dimail API"""
|
||||
@@ -446,7 +413,7 @@ class DimailAPIClient:
|
||||
"surName": mailbox.last_name,
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
},
|
||||
headers=self._get_headers(),
|
||||
headers=self.get_headers(),
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
@@ -458,7 +425,7 @@ class DimailAPIClient:
|
||||
request_user,
|
||||
)
|
||||
return response
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def send_pending_mailboxes(self, domain):
|
||||
"""Send requests for all pending mailboxes of a domain. Returns a list of failed mailboxes for this domain."""
|
||||
@@ -507,7 +474,7 @@ class DimailAPIClient:
|
||||
raise error
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
return response.json()
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def fix_domain(self, domain):
|
||||
"""Send a request to dimail to fix a domain.
|
||||
@@ -524,7 +491,7 @@ class DimailAPIClient:
|
||||
str(domain),
|
||||
)
|
||||
return response.json()
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
def fetch_domain_status(self, domain):
|
||||
"""Send a request to check and update status of a domain."""
|
||||
@@ -649,7 +616,7 @@ class DimailAPIClient:
|
||||
try:
|
||||
response = session.post(
|
||||
f"{self.API_URL}/domains/{mailbox.domain.name}/mailboxes/{mailbox.local_part}/reset_password/",
|
||||
headers=self._get_headers(),
|
||||
headers=self.get_headers(),
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
@@ -663,7 +630,7 @@ class DimailAPIClient:
|
||||
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
# send new password to secondary email
|
||||
self._notify_mailbox_password_reset(
|
||||
self.notify_mailbox_password_reset(
|
||||
recipient=mailbox.secondary_email,
|
||||
mailbox_data={
|
||||
"email": response.json()["email"],
|
||||
@@ -675,183 +642,4 @@ class DimailAPIClient:
|
||||
mailbox,
|
||||
)
|
||||
return response
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
|
||||
def create_alias(self, alias, request_user=None):
|
||||
"""Send a Create alias request to mail provisioning API."""
|
||||
|
||||
payload = {
|
||||
"user_name": alias.local_part,
|
||||
"destination": alias.destination,
|
||||
}
|
||||
headers = self._get_headers()
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
f"{self.API_URL}/domains/{alias.domain.name}/aliases/",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logger.error(
|
||||
"Connection error while trying to reach %s.",
|
||||
self.API_URL,
|
||||
exc_info=error,
|
||||
)
|
||||
raise error
|
||||
|
||||
if response.status_code == status.HTTP_201_CREATED:
|
||||
logger.info(
|
||||
"User %s linked alias %s to a new email.",
|
||||
request_user,
|
||||
f"{alias.local_part}@{alias.domain}",
|
||||
)
|
||||
return response
|
||||
|
||||
if response.status_code == status.HTTP_403_FORBIDDEN:
|
||||
logger.error(
|
||||
"[DIMAIL] 403 Forbidden: you cannot access domain %s",
|
||||
str(alias.domain),
|
||||
)
|
||||
raise exceptions.PermissionDenied(
|
||||
"Permission denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_409_CONFLICT:
|
||||
logger.error(
|
||||
"[DIMAIL] Out of sync with dimail. Admin, please import aliases for domain %s",
|
||||
str(alias.domain),
|
||||
)
|
||||
raise exceptions.ValidationError(
|
||||
{
|
||||
"NON_FIELD_ERRORS": [
|
||||
"Alias already exists. Your domain is out of sync, please contact our support."
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
|
||||
def delete_alias(self, alias, request_user=None):
|
||||
"""Send a Delete alias request to mail provisioning API."""
|
||||
|
||||
headers = self._get_headers()
|
||||
|
||||
try:
|
||||
response = session.delete(
|
||||
f"{self.API_URL}/domains/{alias.domain.name}/aliases/{alias.local_part}/{alias.destination}",
|
||||
json={},
|
||||
headers=headers,
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logger.error(
|
||||
"Connection error while trying to reach %s.",
|
||||
self.API_URL,
|
||||
exc_info=error,
|
||||
)
|
||||
raise error
|
||||
|
||||
if response.status_code == status.HTTP_204_NO_CONTENT:
|
||||
logger.info(
|
||||
"User %s removed destination %s from alias %s.",
|
||||
request_user,
|
||||
alias.destination,
|
||||
f"{alias.local_part}@{alias.domain}",
|
||||
)
|
||||
return response
|
||||
|
||||
if response.status_code == status.HTTP_403_FORBIDDEN:
|
||||
logger.error(
|
||||
"[DIMAIL] 403 Forbidden: you cannot access domain %s",
|
||||
str(alias.domain),
|
||||
)
|
||||
raise exceptions.PermissionDenied(
|
||||
"Permission denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS."
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_404_NOT_FOUND:
|
||||
logger.error(
|
||||
"[DIMAIL] 404, alias %s not found. Domain out of sync with dimail. Admin, please import aliases for domain %s",
|
||||
str(alias.local_part),
|
||||
str(alias.domain),
|
||||
)
|
||||
# we don't raise error because we actually want this alias to be deleted
|
||||
return response
|
||||
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
|
||||
def delete_multiple_alias(self, local_part, domain_name):
|
||||
"""Send a Delete alias request to mail provisioning API."""
|
||||
|
||||
try:
|
||||
response = session.delete(
|
||||
f"{self.API_URL}/domains/{domain_name}/aliases/{local_part}/all",
|
||||
json={},
|
||||
headers=self._get_headers(),
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logger.error(
|
||||
"Connection error while trying to reach %s.",
|
||||
self.API_URL,
|
||||
exc_info=error,
|
||||
)
|
||||
raise error
|
||||
# response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
def import_aliases(self, domain):
|
||||
"""Import aliases from dimail. Useful if people fall out of sync with dimail."""
|
||||
|
||||
try:
|
||||
response = session.get(
|
||||
f"{self.API_URL}/domains/{domain.name}/aliases/",
|
||||
headers=self._get_headers(),
|
||||
verify=True,
|
||||
timeout=self.API_TIMEOUT,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logger.error(
|
||||
"Connection error while trying to reach %s.",
|
||||
self.API_URL,
|
||||
exc_info=error,
|
||||
)
|
||||
raise error
|
||||
|
||||
if response.status_code != status.HTTP_200_OK:
|
||||
return self._raise_exception_for_unexpected_response(response)
|
||||
|
||||
incoming_aliases = response.json()
|
||||
known_aliases = [
|
||||
(known_alias.local_part, known_alias.destination)
|
||||
for known_alias in models.Alias.objects.filter(domain=domain)
|
||||
]
|
||||
|
||||
imported_aliases = []
|
||||
for incoming_alias in incoming_aliases:
|
||||
if (
|
||||
incoming_alias["username"],
|
||||
incoming_alias["destination"],
|
||||
) not in known_aliases:
|
||||
try:
|
||||
new_alias = models.Alias.objects.create(
|
||||
local_part=incoming_alias["username"],
|
||||
destination=incoming_alias["destination"],
|
||||
domain=domain,
|
||||
)
|
||||
except (HeaderParseError, NonASCIILocalPartDefect) as err:
|
||||
logger.warning(
|
||||
"Import of alias %s to %s failed with error %s",
|
||||
incoming_alias["username"],
|
||||
incoming_alias["destination"],
|
||||
err,
|
||||
)
|
||||
else:
|
||||
imported_aliases.append(str(new_alias))
|
||||
return imported_aliases
|
||||
return self.raise_exception_for_unexpected_response(response)
|
||||
|
||||
@@ -74,7 +74,7 @@ class Base(Configuration):
|
||||
You may also want to override default configuration by setting the following environment
|
||||
variables:
|
||||
|
||||
* SENTRY_DSN
|
||||
* DJANGO_SENTRY_DSN
|
||||
* DB_NAME
|
||||
* DB_HOST
|
||||
* DB_PASSWORD
|
||||
@@ -102,7 +102,7 @@ class Base(Configuration):
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": values.Value(
|
||||
"django.db.backends.postgresql",
|
||||
"django.db.backends.postgresql_psycopg2",
|
||||
environ_name="DB_ENGINE",
|
||||
environ_prefix=None,
|
||||
),
|
||||
@@ -173,6 +173,9 @@ class Base(Configuration):
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [
|
||||
os.path.join(BASE_DIR, "templates"),
|
||||
os.path.join(
|
||||
BASE_DIR, "admin", "templates"
|
||||
), # enforce load before Django's admin
|
||||
],
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
@@ -224,6 +227,7 @@ class Base(Configuration):
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# People
|
||||
"admin.apps.PeopleAdminConfig", # replaces 'django.contrib.admin'
|
||||
"core",
|
||||
"demo",
|
||||
"mailbox_manager.apps.MailboxManagerConfig",
|
||||
@@ -242,7 +246,6 @@ class Base(Configuration):
|
||||
"parler",
|
||||
"rest_framework",
|
||||
"treebeard",
|
||||
"lasuite.admin", # must be before django.contrib.admin
|
||||
# Django
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
@@ -251,7 +254,6 @@ class Base(Configuration):
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.admin",
|
||||
# OIDC third party
|
||||
"mozilla_django_oidc",
|
||||
]
|
||||
@@ -339,7 +341,7 @@ class Base(Configuration):
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
|
||||
|
||||
# Sentry
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None)
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN")
|
||||
|
||||
# Easy thumbnails
|
||||
THUMBNAIL_EXTENSION = "webp"
|
||||
@@ -579,14 +581,29 @@ class Base(Configuration):
|
||||
environ_name="MAIL_CHECK_DOMAIN_INTERVAL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
MATRIX_BASE_HOME_SERVER = values.Value(
|
||||
default="https://matrix.agent.dinum.tchap.gouv.fr",
|
||||
environ_name="MATRIX_BASE_HOME_SERVER",
|
||||
DNS_PROVISIONING_TARGET_ZONE = values.Value(
|
||||
default=None,
|
||||
environ_name="DNS_PROVISIONING_TARGET_ZONE",
|
||||
environ_prefix=None,
|
||||
)
|
||||
MATRIX_BOT_ACCESS_TOKEN = values.Value(
|
||||
DNS_PROVISIONING_API_URL = values.Value(
|
||||
default="https://api.scaleway.com",
|
||||
environ_name="DNS_PROVISIONING_API_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
DNS_PROVISIONING_RESOURCE_ID = values.Value(
|
||||
default=None,
|
||||
environ_name="MATRIX_BOT_ACCESS_TOKEN",
|
||||
environ_name="DNS_PROVISIONING_RESOURCE_ID",
|
||||
environ_prefix=None,
|
||||
)
|
||||
DNS_PROVISIONING_API_CREDENTIALS = values.Value(
|
||||
default=None,
|
||||
environ_name="DNS_PROVISIONING_API_CREDENTIALS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
TCHAP_ACCESS_TOKEN = values.Value(
|
||||
default=None,
|
||||
environ_name="TCHAP_ACCESS_TOKEN",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
@@ -1044,6 +1061,10 @@ class Production(Base):
|
||||
},
|
||||
},
|
||||
}
|
||||
SENTRY_DSN = values.Value(
|
||||
"https://b72746c73d669421e7a8ccd3fab0fad2@sentry.incubateur.net/171",
|
||||
environ_name="SENTRY_DSN",
|
||||
)
|
||||
|
||||
|
||||
class Feature(Production):
|
||||
|
||||
@@ -11,9 +11,22 @@ from core.plugins.registry import register_hook
|
||||
from plugins.la_suite.hooks_utils.all_organizations import (
|
||||
get_organization_name_and_metadata_from_siret,
|
||||
)
|
||||
from plugins.la_suite.hooks_utils.communes import CommuneCreation
|
||||
|
||||
|
||||
@register_hook("organization_created")
|
||||
def get_organization_name_and_metadata_from_siret_hook(organization):
|
||||
"""After creating an organization, update the organization name & metadata."""
|
||||
get_organization_name_and_metadata_from_siret(organization)
|
||||
|
||||
|
||||
@register_hook("organization_created")
|
||||
def commune_organization_created(organization):
|
||||
"""After creating an organization, update the organization name."""
|
||||
CommuneCreation().run_after_create(organization)
|
||||
|
||||
|
||||
@register_hook("organization_access_granted")
|
||||
def commune_organization_access_granted(organization_access):
|
||||
"""After granting an organization access, check for needed domain access grant."""
|
||||
CommuneCreation().run_after_grant_access(organization_access)
|
||||
|
||||
@@ -21,45 +21,25 @@ API_URL = "https://recherche-entreprises.api.gouv.fr/search?q={siret}"
|
||||
|
||||
def _get_organization_name_and_metadata_from_results(data, siret):
|
||||
"""Return the organization name and metadata from the results of a SIRET search."""
|
||||
# Find matching organization
|
||||
match = next(
|
||||
(
|
||||
(res, org)
|
||||
for res in data.get("results", [])
|
||||
for org in res.get("matching_etablissements", [])
|
||||
if org.get("siret") == siret
|
||||
),
|
||||
None,
|
||||
)
|
||||
org_metadata = {}
|
||||
for result in data["results"]:
|
||||
for organization in result["matching_etablissements"]:
|
||||
if organization.get("siret") == siret:
|
||||
org_metadata["is_public_service"] = result.get("complements", {}).get(
|
||||
"est_service_public", False
|
||||
)
|
||||
org_metadata["is_commune"] = (
|
||||
str(result.get("nature_juridique", "")) == "7210"
|
||||
)
|
||||
|
||||
if not match:
|
||||
logger.warning("No organization name found for SIRET %s", siret)
|
||||
return None, {}
|
||||
|
||||
result, organization = match
|
||||
|
||||
# Extract metadata
|
||||
is_commune = str(result.get("nature_juridique", "")) == "7210"
|
||||
metadata = {
|
||||
"is_public_service": result.get("complements", {}).get(
|
||||
"est_service_public", False
|
||||
),
|
||||
"is_commune": is_commune,
|
||||
}
|
||||
|
||||
# Extract name (priority: commune name > store signs > business name)
|
||||
name = None
|
||||
if is_commune:
|
||||
name = result.get("siege", {}).get("libelle_commune")
|
||||
if not name: # Fallback for non-communes OR if commune has no libelle_commune
|
||||
store_signs = organization.get("liste_enseignes") or []
|
||||
name = store_signs[0] if store_signs else result.get("nom_raison_sociale")
|
||||
|
||||
if name:
|
||||
return name.title(), metadata
|
||||
store_signs = organization.get("liste_enseignes") or []
|
||||
if store_signs:
|
||||
return store_signs[0].title(), org_metadata
|
||||
if name := result.get("nom_raison_sociale"):
|
||||
return name.title(), org_metadata
|
||||
|
||||
logger.warning("No organization name found for SIRET %s", siret)
|
||||
return None, metadata
|
||||
return None, org_metadata
|
||||
|
||||
|
||||
def get_organization_name_and_metadata_from_siret(organization):
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
"""Organization related plugins."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
from mailbox_manager.enums import MailDomainRoleChoices
|
||||
from mailbox_manager.models import MailDomain, MailDomainAccess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiCall:
|
||||
"""Encapsulates a call to an external API"""
|
||||
|
||||
inputs: dict = {}
|
||||
method: str = "GET"
|
||||
base: str = ""
|
||||
url: str = ""
|
||||
params: dict = {}
|
||||
headers: dict = {}
|
||||
response_data = None
|
||||
|
||||
def execute(self):
|
||||
"""Call the specified API endpoint with supplied parameters and record response"""
|
||||
if self.method in ("POST", "PATCH"):
|
||||
response = requests.request(
|
||||
method=self.method,
|
||||
url=f"{self.base}/{self.url}",
|
||||
json=self.params,
|
||||
headers=self.headers,
|
||||
timeout=20,
|
||||
)
|
||||
else:
|
||||
response = requests.request(
|
||||
method=self.method,
|
||||
url=f"{self.base}/{self.url}",
|
||||
params=self.params,
|
||||
headers=self.headers,
|
||||
timeout=20,
|
||||
)
|
||||
self.response_data = response.json()
|
||||
logger.info(
|
||||
"API call: %s %s %s %s",
|
||||
self.method,
|
||||
self.url,
|
||||
self.params,
|
||||
self.response_data,
|
||||
)
|
||||
|
||||
|
||||
class CommuneCreation:
|
||||
"""
|
||||
This plugin handles setup tasks for French communes.
|
||||
"""
|
||||
|
||||
_api_url = "https://recherche-entreprises.api.gouv.fr/search?q={siret}"
|
||||
|
||||
def get_organization_name_from_results(self, data, siret):
|
||||
"""Return the organization name from the results of a SIRET search."""
|
||||
for result in data["results"]:
|
||||
nature = "nature_juridique"
|
||||
commune = nature in result and result[nature] == "7210"
|
||||
if commune:
|
||||
return result["siege"]["libelle_commune"].title()
|
||||
|
||||
logger.warning("Not a commune: SIRET %s", siret)
|
||||
return None
|
||||
|
||||
def dns_call(self, spec):
|
||||
"""Call to add a DNS record"""
|
||||
zone_name = self.zone_name(spec.inputs["name"])
|
||||
|
||||
records = [
|
||||
{
|
||||
"name": item["target"],
|
||||
"type": item["type"].upper(),
|
||||
"data": item["value"],
|
||||
"ttl": 3600,
|
||||
}
|
||||
for item in spec.response_data
|
||||
]
|
||||
result = ApiCall()
|
||||
result.method = "PATCH"
|
||||
result.base = "https://api.scaleway.com"
|
||||
result.url = f"/domain/v2beta1/dns-zones/{zone_name}/records"
|
||||
result.params = {"changes": [{"add": {"records": records}}]}
|
||||
result.headers = {"X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS}
|
||||
return result
|
||||
|
||||
def normalize_name(self, name: str) -> str:
|
||||
"""Map the name to a standard form"""
|
||||
name = re.sub("'", "-", name)
|
||||
return slugify(name)
|
||||
|
||||
def zone_name(self, name: str) -> str:
|
||||
"""Derive the zone name from the commune name"""
|
||||
normalized = self.normalize_name(name)
|
||||
return f"{normalized}.{settings.DNS_PROVISIONING_TARGET_ZONE}"
|
||||
|
||||
def complete_commune_creation(self, name: str) -> ApiCall:
|
||||
"""Specify the tasks to be completed after a commune is created."""
|
||||
inputs = {"name": self.normalize_name(name)}
|
||||
|
||||
create_zone = ApiCall()
|
||||
create_zone.method = "POST"
|
||||
create_zone.base = "https://api.scaleway.com"
|
||||
create_zone.url = "/domain/v2beta1/dns-zones"
|
||||
create_zone.params = {
|
||||
"project_id": settings.DNS_PROVISIONING_RESOURCE_ID,
|
||||
"domain": settings.DNS_PROVISIONING_TARGET_ZONE,
|
||||
"subdomain": inputs["name"],
|
||||
}
|
||||
create_zone.headers = {
|
||||
"X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS
|
||||
}
|
||||
|
||||
zone_name = self.zone_name(inputs["name"])
|
||||
|
||||
create_domain = ApiCall()
|
||||
create_domain.method = "POST"
|
||||
create_domain.base = settings.MAIL_PROVISIONING_API_URL
|
||||
create_domain.url = "/domains/"
|
||||
create_domain.params = {
|
||||
"name": zone_name,
|
||||
"delivery": "virtual",
|
||||
"features": ["webmail", "mailbox"],
|
||||
"context_name": zone_name,
|
||||
}
|
||||
create_domain.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
spec_domain = ApiCall()
|
||||
spec_domain.inputs = inputs
|
||||
spec_domain.base = settings.MAIL_PROVISIONING_API_URL
|
||||
spec_domain.url = f"/domains/{zone_name}/spec"
|
||||
spec_domain.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
return [create_zone, create_domain, spec_domain]
|
||||
|
||||
def complete_zone_creation(self, spec_call):
|
||||
"""Specify the tasks to be performed to set up the zone."""
|
||||
return self.dns_call(spec_call)
|
||||
|
||||
def run_after_create(self, organization):
|
||||
"""After creating an organization, update the organization name."""
|
||||
logger.info("In CommuneCreation")
|
||||
if not organization.registration_id_list:
|
||||
# No registration ID to convert...
|
||||
return
|
||||
|
||||
# In the nominal case, there is only one registration ID because
|
||||
# the organization has been created from it.
|
||||
try:
|
||||
# Retry logic as the API may be rate limited
|
||||
s = requests.Session()
|
||||
retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[429])
|
||||
s.mount("https://", HTTPAdapter(max_retries=retries))
|
||||
|
||||
siret = organization.registration_id_list[0]
|
||||
response = s.get(self._api_url.format(siret=siret), timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
name = self.get_organization_name_from_results(data, siret)
|
||||
# Not a commune ?
|
||||
if not name:
|
||||
return
|
||||
except requests.RequestException as exc:
|
||||
logger.exception("%s: Unable to fetch organization name from SIRET", exc)
|
||||
return
|
||||
|
||||
organization.name = name
|
||||
organization.save(update_fields=["name", "updated_at"])
|
||||
logger.info("Organization %s name updated to %s", organization, name)
|
||||
|
||||
zone_name = self.zone_name(name)
|
||||
support = "support-regie@numerique.gouv.fr"
|
||||
MailDomain.objects.get_or_create(name=zone_name, support_email=support)
|
||||
|
||||
# Compute and execute the rest of the process
|
||||
tasks = self.complete_commune_creation(name)
|
||||
for task in tasks:
|
||||
task.execute()
|
||||
last_task = self.complete_zone_creation(tasks[-1])
|
||||
last_task.execute()
|
||||
|
||||
def complete_grant_access(self, sub, zone_name):
|
||||
"""Specify the tasks to be completed after making a user admin"""
|
||||
create_user = ApiCall()
|
||||
create_user.method = "POST"
|
||||
create_user.base = settings.MAIL_PROVISIONING_API_URL
|
||||
create_user.url = "/users/"
|
||||
create_user.params = {
|
||||
"name": sub,
|
||||
"password": "no",
|
||||
"is_admin": False,
|
||||
"perms": [],
|
||||
}
|
||||
create_user.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
grant_access = ApiCall()
|
||||
grant_access.method = "POST"
|
||||
grant_access.base = settings.MAIL_PROVISIONING_API_URL
|
||||
grant_access.url = "/allows/"
|
||||
grant_access.params = {
|
||||
"user": sub,
|
||||
"domain": zone_name,
|
||||
}
|
||||
grant_access.headers = {
|
||||
"Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
}
|
||||
|
||||
return [create_user, grant_access]
|
||||
|
||||
def run_after_grant_access(self, organization_access):
|
||||
"""After granting an organization access, check for needed domain access grant."""
|
||||
orga = organization_access.organization
|
||||
user = organization_access.user
|
||||
zone_name = self.zone_name(orga.name)
|
||||
|
||||
try:
|
||||
domain = MailDomain.objects.get(name=zone_name)
|
||||
except MailDomain.DoesNotExist:
|
||||
domain = None
|
||||
|
||||
if domain:
|
||||
MailDomainAccess.objects.create(
|
||||
domain=domain, user=user, role=MailDomainRoleChoices.OWNER
|
||||
)
|
||||
|
||||
tasks = self.complete_grant_access(user.sub, zone_name)
|
||||
for task in tasks:
|
||||
task.execute()
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Tests for the CommuneCreation plugin."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
|
||||
from plugins.la_suite.hooks_utils.communes import ApiCall, CommuneCreation
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_extract_name_from_org_data_when_commune():
|
||||
"""Test the name is extracted correctly for a French commune."""
|
||||
data = {
|
||||
"results": [
|
||||
{
|
||||
"nom_complet": "COMMUNE DE VARZY",
|
||||
"nom_raison_sociale": "COMMUNE DE VARZY",
|
||||
"siege": {
|
||||
"libelle_commune": "VARZY",
|
||||
"liste_enseignes": ["MAIRIE"],
|
||||
"siret": "21580304000017",
|
||||
},
|
||||
"nature_juridique": "7210",
|
||||
"matching_etablissements": [
|
||||
{
|
||||
"siret": "21580304000017",
|
||||
"libelle_commune": "VARZY",
|
||||
"liste_enseignes": ["MAIRIE"],
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
plugin = CommuneCreation()
|
||||
name = plugin.get_organization_name_from_results(data, "21580304000017")
|
||||
assert name == "Varzy"
|
||||
|
||||
|
||||
def test_api_call_execution():
|
||||
"""Test that calling execute() faithfully executes the API call"""
|
||||
task = ApiCall()
|
||||
task.method = "POST"
|
||||
task.base = "https://some_host"
|
||||
task.url = "some_url"
|
||||
task.params = {"some_key": "some_value"}
|
||||
task.headers = {"Some-Header": "Some-Header-Value"}
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
url="https://some_host/some_url",
|
||||
body='{"some_key": "some_value"}',
|
||||
content_type="application/json",
|
||||
headers={"Some-Header": "Some-Header-Value"},
|
||||
)
|
||||
|
||||
task.execute()
|
||||
|
||||
|
||||
@override_settings(DNS_PROVISIONING_TARGET_ZONE="collectivite.fr")
|
||||
def test_tasks_on_commune_creation_include_zone_creation():
|
||||
"""Test the first task in commune creation: creating the DNS sub-zone"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Varzy"
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
|
||||
assert tasks[0].base == "https://api.scaleway.com"
|
||||
assert tasks[0].url == "/domain/v2beta1/dns-zones"
|
||||
assert tasks[0].method == "POST"
|
||||
assert tasks[0].params == {
|
||||
"project_id": settings.DNS_PROVISIONING_RESOURCE_ID,
|
||||
"domain": "collectivite.fr",
|
||||
"subdomain": "varzy",
|
||||
}
|
||||
assert tasks[0].headers["X-Auth-Token"] == settings.DNS_PROVISIONING_API_CREDENTIALS
|
||||
|
||||
|
||||
@override_settings(DNS_PROVISIONING_TARGET_ZONE="collectivite.fr")
|
||||
def test_tasks_on_commune_creation_include_dimail_domain_creation():
|
||||
"""Test the second task in commune creation: creating the domain in Dimail"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Merlaut"
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
|
||||
assert tasks[1].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[1].url == "/domains/"
|
||||
assert tasks[1].method == "POST"
|
||||
assert tasks[1].params == {
|
||||
"name": "merlaut.collectivite.fr",
|
||||
"delivery": "virtual",
|
||||
"features": ["webmail", "mailbox"],
|
||||
"context_name": "merlaut.collectivite.fr",
|
||||
}
|
||||
assert (
|
||||
tasks[1].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
|
||||
@override_settings(DNS_PROVISIONING_TARGET_ZONE="collectivite.fr")
|
||||
def test_tasks_on_commune_creation_include_fetching_spec():
|
||||
"""Test the third task in commune creation: asking Dimail for the spec"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Loc-Eguiner"
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
|
||||
assert tasks[2].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[2].url == "/domains/loc-eguiner.collectivite.fr/spec"
|
||||
assert tasks[2].method == "GET"
|
||||
assert (
|
||||
tasks[2].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
|
||||
@override_settings(DNS_PROVISIONING_TARGET_ZONE="collectivite.fr")
|
||||
def test_tasks_on_commune_creation_include_dns_records():
|
||||
"""Test the next several tasks in commune creation: creating records"""
|
||||
plugin = CommuneCreation()
|
||||
name = "Abidos"
|
||||
|
||||
spec_response = [
|
||||
{"target": "", "type": "mx", "value": "mx.dev.ox.numerique.gouv.fr."},
|
||||
{
|
||||
"target": "dimail._domainkey",
|
||||
"type": "txt",
|
||||
"value": "v=DKIM1; h=sha256; k=rsa; p=MIICIjANB<truncated>AAQ==",
|
||||
},
|
||||
{"target": "imap", "type": "cname", "value": "imap.dev.ox.numerique.gouv.fr."},
|
||||
{"target": "smtp", "type": "cname", "value": "smtp.dev.ox.numerique.gouv.fr."},
|
||||
{
|
||||
"target": "",
|
||||
"type": "txt",
|
||||
"value": "v=spf1 include:_spf.dev.ox.numerique.gouv.fr -all",
|
||||
},
|
||||
{
|
||||
"target": "webmail",
|
||||
"type": "cname",
|
||||
"value": "webmail.dev.ox.numerique.gouv.fr.",
|
||||
},
|
||||
]
|
||||
|
||||
tasks = plugin.complete_commune_creation(name)
|
||||
tasks[2].response_data = spec_response
|
||||
|
||||
expected = {
|
||||
"changes": [
|
||||
{
|
||||
"add": {
|
||||
"records": [
|
||||
{
|
||||
"name": item["target"],
|
||||
"type": item["type"].upper(),
|
||||
"data": item["value"],
|
||||
"ttl": 3600,
|
||||
}
|
||||
for item in spec_response
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
zone_call = plugin.complete_zone_creation(tasks[2])
|
||||
assert zone_call.params == expected
|
||||
assert zone_call.url == "/domain/v2beta1/dns-zones/abidos.collectivite.fr/records"
|
||||
assert (
|
||||
zone_call.headers["X-Auth-Token"] == settings.DNS_PROVISIONING_API_CREDENTIALS
|
||||
)
|
||||
|
||||
|
||||
@override_settings(DNS_PROVISIONING_TARGET_ZONE="collectivite.fr")
|
||||
def test_tasks_on_grant_access():
|
||||
"""Test the final tasks after making user admin of an org"""
|
||||
plugin = CommuneCreation()
|
||||
|
||||
tasks = plugin.complete_grant_access("some-sub", "mezos.collectivite.fr")
|
||||
|
||||
assert tasks[0].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[0].url == "/users/"
|
||||
assert tasks[0].method == "POST"
|
||||
assert tasks[0].params == {
|
||||
"name": "some-sub",
|
||||
"password": "no",
|
||||
"is_admin": False,
|
||||
"perms": [],
|
||||
}
|
||||
assert (
|
||||
tasks[0].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
assert tasks[1].base == settings.MAIL_PROVISIONING_API_URL
|
||||
assert tasks[1].url == "/allows/"
|
||||
assert tasks[1].method == "POST"
|
||||
assert tasks[1].params == {
|
||||
"user": "some-sub",
|
||||
"domain": "mezos.collectivite.fr",
|
||||
}
|
||||
assert (
|
||||
tasks[1].headers["Authorization"]
|
||||
== f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_name():
|
||||
"""Test name normalization"""
|
||||
plugin = CommuneCreation()
|
||||
assert plugin.normalize_name("Asnières-sur-Saône") == "asnieres-sur-saone"
|
||||
assert plugin.normalize_name("Bâgé-le-Châtel") == "bage-le-chatel"
|
||||
assert plugin.normalize_name("Courçais") == "courcais"
|
||||
assert plugin.normalize_name("Moÿ-de-l'Aisne") == "moy-de-l-aisne"
|
||||
assert plugin.normalize_name("Salouël") == "salouel"
|
||||
assert (
|
||||
plugin.normalize_name("Bors (Canton de Tude-et-Lavalette)")
|
||||
== "bors-canton-de-tude-et-lavalette"
|
||||
)
|
||||
|
||||
|
||||
@override_settings(DNS_PROVISIONING_TARGET_ZONE="collectivite.fr")
|
||||
def test_zone_name():
|
||||
"""Test transforming a commune name to a sub-zone of collectivite.fr"""
|
||||
plugin = CommuneCreation()
|
||||
assert plugin.zone_name("Bâgé-le-Châtel") == "bage-le-chatel.collectivite.fr"
|
||||
@@ -26,6 +26,15 @@ def test_hooks_loaded():
|
||||
]
|
||||
assert organization_created_hook_names == [
|
||||
"get_organization_name_and_metadata_from_siret_hook",
|
||||
"commune_organization_created",
|
||||
]
|
||||
|
||||
organization_access_granted_hook_names = [
|
||||
callback.__name__
|
||||
for callback in registry.get_callbacks("organization_access_granted")
|
||||
]
|
||||
assert organization_access_granted_hook_names == [
|
||||
"commune_organization_access_granted"
|
||||
]
|
||||
|
||||
# cleanup the hooks
|
||||
|
||||
+45
-46
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "people"
|
||||
version = "1.24.0"
|
||||
version = "1.17.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -17,51 +17,50 @@ classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
description = "An application to handle contacts and teams."
|
||||
keywords = ["Django", "Contacts", "Teams", "RBAC"]
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = "~=3.14.2"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"Brotli==1.2.0",
|
||||
"PyJWT==2.12.0",
|
||||
"boto3==1.42.62",
|
||||
"celery[redis]==5.6.2",
|
||||
"django-celery-beat==2.9.0",
|
||||
"Brotli==1.1.0",
|
||||
"PyJWT==2.10.1",
|
||||
"boto3==1.38.33",
|
||||
"celery[redis]==5.5.3",
|
||||
"django-celery-beat==2.8.1",
|
||||
"django-celery-results==2.6.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==8.2.0",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-extensions==4.1",
|
||||
"django-lasuite==0.0.25",
|
||||
"django-oauth-toolkit==3.2.0",
|
||||
"django-lasuite==0.0.9",
|
||||
"django-oauth-toolkit==3.0.1",
|
||||
"django-parler==2.3",
|
||||
"django-redis==6.0.0",
|
||||
"django-redis==5.4.0",
|
||||
"django-storages==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django-treebeard==5.0.5",
|
||||
"django-zxcvbn-password-validator==1.5.3",
|
||||
"django==6.0.3",
|
||||
"djangorestframework==3.16.1",
|
||||
"dockerflow==2026.3.4",
|
||||
"drf_spectacular==0.29.0",
|
||||
"drf_spectacular[sidecar]==0.29.0",
|
||||
"easy_thumbnails==2.10.1",
|
||||
"django-treebeard==4.7.1",
|
||||
"django-zxcvbn-password-validator==1.4.5",
|
||||
"django==5.2.3",
|
||||
"djangorestframework==3.16.0",
|
||||
"dockerflow==2024.4.2",
|
||||
"drf_spectacular==0.28.0",
|
||||
"drf_spectacular[sidecar]==0.28.0",
|
||||
"easy_thumbnails==2.10",
|
||||
"factory_boy==3.3.3",
|
||||
"flower==2.0.1",
|
||||
"gunicorn==25.1.0",
|
||||
"jaraco.context>=6.1.0",
|
||||
"joserfc==1.6.3",
|
||||
"jsonschema==4.26.0",
|
||||
"mozilla-django-oidc==5.0.2",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"psycopg[binary]==3.3.3",
|
||||
"redis<=7.2.1",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk[django]==2.54.0",
|
||||
"whitenoise==6.12.0",
|
||||
"gunicorn==23.0.0",
|
||||
"joserfc==1.1.0",
|
||||
"jsonschema==4.24.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"redis==5.2.1",
|
||||
"requests==2.32.4",
|
||||
"sentry-sdk[django]==2.29.1",
|
||||
"whitenoise==6.9.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -72,22 +71,22 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"drf-spectacular-sidecar==2026.3.1",
|
||||
"drf-spectacular-sidecar==2025.6.1",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.11.0",
|
||||
"jq==1.11.0",
|
||||
"pyfakefs==6.1.4",
|
||||
"pylint-django==2.7.0",
|
||||
"pylint==4.0.5",
|
||||
"pytest-cov==7.0.0",
|
||||
"pytest-django==4.12.0",
|
||||
"pytest==9.0.2",
|
||||
"ipython==9.3.0",
|
||||
"jq==1.8.0",
|
||||
"pyfakefs==5.8.0",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.7",
|
||||
"pytest-cov==6.1.1",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==8.4.0",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.26.0",
|
||||
"ruff==0.15.5",
|
||||
"types-requests==2.32.4.20260107",
|
||||
"freezegun==1.5.5",
|
||||
"pytest-xdist==3.7.0",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.11.13",
|
||||
"types-requests==2.32.0.20250602",
|
||||
"freezegun==1.5.2",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
Generated
-1803
File diff suppressed because it is too large
Load Diff
@@ -47,13 +47,6 @@ RUN yarn build
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
# Upgrade system packages to install security updates
|
||||
USER root
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
rm -rf /var/cache/apk/*
|
||||
USER -
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
@@ -1 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
@@ -1,864 +1,5 @@
|
||||
import { getThemesFromGlobals } from '@gouvfr-lasuite/cunningham-tokens';
|
||||
import { cunninghamConfig } from '@gouvfr-lasuite/ui-kit';
|
||||
|
||||
export const commonTokenOverrides = {
|
||||
components: {
|
||||
favicon: {
|
||||
'png-light': '/assets/favicon-light.png',
|
||||
'png-dark': '/assets/favicon-dark.png',
|
||||
},
|
||||
modal: {
|
||||
'width-small': '342px',
|
||||
},
|
||||
tooltip: {
|
||||
padding: '4px 8px',
|
||||
},
|
||||
button: {
|
||||
'medium-height': '40px',
|
||||
'medium-text-height': '40px',
|
||||
'border-radius': '4px',
|
||||
'border-radius--active': '4px',
|
||||
'border-radius--focus': '4px',
|
||||
},
|
||||
datagrid: {
|
||||
'header--color': 'ref(contextuals.content.semantic.neutral.primary)',
|
||||
'header--size': '12px',
|
||||
'header--weight': '500',
|
||||
'body--background-color-hover':
|
||||
'ref(contextuals.background.semantic.neutral.tertiary)',
|
||||
},
|
||||
'forms-checkbox': {
|
||||
'font-size': 'ref(globals.font.sizes.sm)',
|
||||
},
|
||||
badge: {
|
||||
'font-size': 'ref(globals.font.sizes.xs)',
|
||||
'border-radius': '12px',
|
||||
'padding-inline': 'ref(globals.spacings.xs)',
|
||||
'padding-block': 'ref(globals.spacings.2xs)',
|
||||
accent: {
|
||||
'background-color':
|
||||
'ref(contextuals.background.semantic.brand.secondary)',
|
||||
color: 'ref(contextuals.content.semantic.brand.secondary)',
|
||||
},
|
||||
neutral: {
|
||||
'background-color':
|
||||
'ref(contextuals.background.semantic.neutral.secondary)',
|
||||
color: 'ref(contextuals.content.semantic.neutral.secondary)',
|
||||
},
|
||||
danger: {
|
||||
'background-color':
|
||||
'ref(contextuals.background.semantic.error.secondary)',
|
||||
color: 'ref(contextuals.content.semantic.error.secondary)',
|
||||
},
|
||||
success: {
|
||||
'background-color':
|
||||
'ref(contextuals.background.semantic.success.secondary)',
|
||||
color: 'ref(contextuals.content.semantic.success.secondary)',
|
||||
},
|
||||
warning: {
|
||||
'background-color':
|
||||
'ref(contextuals.background.semantic.warning.secondary)',
|
||||
color: 'ref(contextuals.content.semantic.warning.secondary)',
|
||||
},
|
||||
info: {
|
||||
'background-color':
|
||||
'ref(contextuals.background.semantic.info.secondary)',
|
||||
color: 'ref(contextuals.content.semantic.info.secondary)',
|
||||
},
|
||||
},
|
||||
},
|
||||
export default {
|
||||
...cunninghamConfig,
|
||||
};
|
||||
|
||||
export const commonGlobals = {
|
||||
components: {
|
||||
'la-gaufre': false,
|
||||
'home-proconnect': false,
|
||||
logo: {
|
||||
src: '',
|
||||
alt: '',
|
||||
widthHeader: '',
|
||||
widthFooter: '',
|
||||
},
|
||||
},
|
||||
font: {
|
||||
sizes: {
|
||||
xs: '0.75rem',
|
||||
sm: '0.875rem',
|
||||
md: '1rem',
|
||||
lg: '1.125rem',
|
||||
ml: '0.938rem',
|
||||
xl: '1.25rem',
|
||||
t: '0.6875rem',
|
||||
s: '0.75rem',
|
||||
h1: '2rem',
|
||||
h2: '1.75rem',
|
||||
h3: '1.5rem',
|
||||
h4: '1.375rem',
|
||||
h5: '1.25rem',
|
||||
h6: '1.125rem',
|
||||
'xl-alt': '5rem',
|
||||
'lg-alt': '4.5rem',
|
||||
'md-alt': '4rem',
|
||||
'sm-alt': '3.5rem',
|
||||
'xs-alt': '3rem',
|
||||
},
|
||||
weights: {
|
||||
thin: 100,
|
||||
extrabold: 800,
|
||||
black: 900,
|
||||
},
|
||||
families: {
|
||||
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
|
||||
},
|
||||
},
|
||||
spacings: {
|
||||
'0': '0',
|
||||
none: '0',
|
||||
auto: 'auto',
|
||||
bx: '2.2rem',
|
||||
full: '100%',
|
||||
'4xs': '0.125rem',
|
||||
'3xs': '0.25rem',
|
||||
'2xs': '0.375rem',
|
||||
xs: '0.5rem',
|
||||
sm: '0.75rem',
|
||||
base: '1rem',
|
||||
md: '1.5rem',
|
||||
lg: '2rem',
|
||||
xl: '2.5rem',
|
||||
xxl: '3rem',
|
||||
'2xl': '3rem',
|
||||
xxxl: '3.5rem',
|
||||
'3xl': '3.5rem',
|
||||
'4xl': '4rem',
|
||||
'5xl': '4.5rem',
|
||||
'6xl': '6rem',
|
||||
'7xl': '7.5rem',
|
||||
},
|
||||
breakpoints: {
|
||||
xxs: '320px',
|
||||
xs: '480px',
|
||||
mobile: '768px',
|
||||
tablet: '1024px',
|
||||
},
|
||||
};
|
||||
|
||||
export const whiteLabelGlobals = {
|
||||
colors: {
|
||||
'logo-1-light': '#4844AD',
|
||||
'logo-2-light': '#4844AD',
|
||||
'logo-1-dark': '#BEC5F0',
|
||||
'logo-2-dark': '#BEC5F0',
|
||||
'brand-050': '#EEF1FA',
|
||||
'brand-100': '#DDE2F5',
|
||||
'brand-150': '#CED3F1',
|
||||
'brand-200': '#BEC5F0',
|
||||
'brand-250': '#AFB5F1',
|
||||
'brand-300': '#A0A5F6',
|
||||
'brand-350': '#8F94FD',
|
||||
'brand-400': '#8184FC',
|
||||
'brand-450': '#7576EE',
|
||||
'brand-500': '#6969DF',
|
||||
'brand-550': '#5E5CD0',
|
||||
'brand-600': '#534FC2',
|
||||
'brand-650': '#4844AD',
|
||||
'brand-700': '#3E3B98',
|
||||
'brand-750': '#36347D',
|
||||
'brand-800': '#2D2F5F',
|
||||
'brand-850': '#262848',
|
||||
'brand-900': '#1C1E32',
|
||||
'brand-950': '#11131F',
|
||||
'gray-000': '#FFFFFF',
|
||||
'gray-025': '#F8F8F9',
|
||||
'gray-050': '#F0F0F3',
|
||||
'gray-100': '#E2E2EA',
|
||||
'gray-150': '#D3D4E0',
|
||||
'gray-200': '#C5C6D5',
|
||||
'gray-250': '#B7B7CB',
|
||||
'gray-300': '#A9A9BF',
|
||||
'gray-350': '#9C9CB2',
|
||||
'gray-400': '#8F8FA4',
|
||||
'gray-450': '#828297',
|
||||
'gray-500': '#75758A',
|
||||
'gray-550': '#69697D',
|
||||
'gray-600': '#5D5D70',
|
||||
'gray-650': '#515164',
|
||||
'gray-700': '#454558',
|
||||
'gray-750': '#3A3A4C',
|
||||
'gray-800': '#2F303D',
|
||||
'gray-850': '#25252F',
|
||||
'gray-900': '#1B1B23',
|
||||
'gray-950': '#111114',
|
||||
'gray-1000': '#000000',
|
||||
'info-050': '#EAF2F9',
|
||||
'info-100': '#D5E4F3',
|
||||
'info-150': '#BFD7F0',
|
||||
'info-200': '#A7CAEE',
|
||||
'info-250': '#8DBDEF',
|
||||
'info-300': '#6EB0F2',
|
||||
'info-350': '#50A2F5',
|
||||
'info-400': '#3593F4',
|
||||
'info-450': '#1185ED',
|
||||
'info-500': '#0077DE',
|
||||
'info-550': '#0069CF',
|
||||
'info-600': '#005BC0',
|
||||
'info-650': '#0D4EAA',
|
||||
'info-700': '#124394',
|
||||
'info-750': '#163878',
|
||||
'info-800': '#192F5A',
|
||||
'info-850': '#192541',
|
||||
'info-900': '#141B2D',
|
||||
'info-950': '#0C111C',
|
||||
'success-050': '#E8F1EA',
|
||||
'success-100': '#CFE4D4',
|
||||
'success-150': '#BAD9C1',
|
||||
'success-200': '#A2CFAD',
|
||||
'success-250': '#86C597',
|
||||
'success-300': '#6CBA83',
|
||||
'success-350': '#4FB070',
|
||||
'success-400': '#40A363',
|
||||
'success-450': '#309556',
|
||||
'success-500': '#1E884A',
|
||||
'success-550': '#027B3E',
|
||||
'success-600': '#016D31',
|
||||
'success-650': '#006024',
|
||||
'success-700': '#005317',
|
||||
'success-750': '#0D4511',
|
||||
'success-800': '#11380E',
|
||||
'success-850': '#132A11',
|
||||
'success-900': '#101E0F',
|
||||
'success-950': '#091209',
|
||||
'warning-050': '#F8F0E9',
|
||||
'warning-100': '#F1E0D3',
|
||||
'warning-150': '#ECD0BC',
|
||||
'warning-200': '#E8C0A4',
|
||||
'warning-250': '#E8AE8A',
|
||||
'warning-300': '#EB9970',
|
||||
'warning-350': '#E98456',
|
||||
'warning-400': '#E57036',
|
||||
'warning-450': '#DA5E18',
|
||||
'warning-500': '#CB5000',
|
||||
'warning-550': '#BC4200',
|
||||
'warning-600': '#AD3300',
|
||||
'warning-650': '#9E2300',
|
||||
'warning-700': '#882011',
|
||||
'warning-750': '#731E16',
|
||||
'warning-800': '#58201A',
|
||||
'warning-850': '#401D18',
|
||||
'warning-900': '#2E1714',
|
||||
'warning-950': '#1D0F0D',
|
||||
'error-050': '#F9EFEC',
|
||||
'error-100': '#F4DFD9',
|
||||
'error-150': '#F0CEC6',
|
||||
'error-200': '#EEBCB2',
|
||||
'error-250': '#EEA99D',
|
||||
'error-300': '#EF9486',
|
||||
'error-350': '#F37C6E',
|
||||
'error-400': '#F65F53',
|
||||
'error-450': '#F0463D',
|
||||
'error-500': '#E82322',
|
||||
'error-550': '#D7010E',
|
||||
'error-600': '#C00100',
|
||||
'error-650': '#AA0000',
|
||||
'error-700': '#910C06',
|
||||
'error-750': '#731E16',
|
||||
'error-800': '#58201A',
|
||||
'error-850': '#401D18',
|
||||
'error-900': '#2E1714',
|
||||
'error-950': '#1D0F0D',
|
||||
'red-050': '#FAEFEE',
|
||||
'red-100': '#F4DEDD',
|
||||
'red-150': '#F1CDCB',
|
||||
'red-200': '#EFBBBA',
|
||||
'red-250': '#EEA8A8',
|
||||
'red-300': '#F09394',
|
||||
'red-350': '#F37B7E',
|
||||
'red-400': '#EF6569',
|
||||
'red-450': '#E94A55',
|
||||
'red-500': '#DA3B49',
|
||||
'red-550': '#CA2A3C',
|
||||
'red-600': '#BB1330',
|
||||
'red-650': '#A90021',
|
||||
'red-700': '#910A13',
|
||||
'red-750': '#731E16',
|
||||
'red-800': '#58201A',
|
||||
'red-850': '#411D18',
|
||||
'red-900': '#2E1714',
|
||||
'red-950': '#1D0F0D',
|
||||
'orange-050': '#F8F0E9',
|
||||
'orange-100': '#F1E0D3',
|
||||
'orange-150': '#ECD0BD',
|
||||
'orange-200': '#EABFA6',
|
||||
'orange-250': '#EBAC90',
|
||||
'orange-300': '#EC9772',
|
||||
'orange-350': '#E5845A',
|
||||
'orange-400': '#D6774D',
|
||||
'orange-450': '#C86A40',
|
||||
'orange-500': '#B95D33',
|
||||
'orange-550': '#AB5025',
|
||||
'orange-600': '#9D4315',
|
||||
'orange-650': '#8F3600',
|
||||
'orange-700': '#812900',
|
||||
'orange-750': '#6C2511',
|
||||
'orange-800': '#572017',
|
||||
'orange-850': '#401D18',
|
||||
'orange-900': '#2E1714',
|
||||
'orange-950': '#1D0F0D',
|
||||
'brown-050': '#F6F0E8',
|
||||
'brown-100': '#F1E0D3',
|
||||
'brown-150': '#EBD0BA',
|
||||
'brown-200': '#E2C0A6',
|
||||
'brown-250': '#D4B398',
|
||||
'brown-300': '#C6A58B',
|
||||
'brown-350': '#B8987E',
|
||||
'brown-400': '#AA8B71',
|
||||
'brown-450': '#9D7E65',
|
||||
'brown-500': '#8F7158',
|
||||
'brown-550': '#82654C',
|
||||
'brown-600': '#765841',
|
||||
'brown-650': '#694C35',
|
||||
'brown-700': '#5D412A',
|
||||
'brown-750': '#51361E',
|
||||
'brown-800': '#452A13',
|
||||
'brown-850': '#392008',
|
||||
'brown-900': '#29180A',
|
||||
'brown-950': '#1B0F08',
|
||||
'yellow-050': '#F3F0E7',
|
||||
'yellow-100': '#E9E2CF',
|
||||
'yellow-150': '#E1D4B7',
|
||||
'yellow-200': '#D9C599',
|
||||
'yellow-250': '#D2B677',
|
||||
'yellow-300': '#CAA756',
|
||||
'yellow-350': '#C2972E',
|
||||
'yellow-400': '#B98900',
|
||||
'yellow-450': '#AB7B00',
|
||||
'yellow-500': '#9D6E00',
|
||||
'yellow-550': '#916100',
|
||||
'yellow-600': '#855400',
|
||||
'yellow-650': '#784700',
|
||||
'yellow-700': '#6C3A00',
|
||||
'yellow-750': '#5F2E00',
|
||||
'yellow-800': '#512302',
|
||||
'yellow-850': '#3E1D10',
|
||||
'yellow-900': '#2D1711',
|
||||
'yellow-950': '#1D0F0D',
|
||||
'green-050': '#E6F1E9',
|
||||
'green-100': '#CFE4D5',
|
||||
'green-150': '#B8D8C1',
|
||||
'green-200': '#A0CFAE',
|
||||
'green-250': '#84C59A',
|
||||
'green-300': '#65BA86',
|
||||
'green-350': '#45B173',
|
||||
'green-400': '#23A562',
|
||||
'green-450': '#029755',
|
||||
'green-500': '#008948',
|
||||
'green-550': '#017B3B',
|
||||
'green-600': '#006E2E',
|
||||
'green-650': '#006022',
|
||||
'green-700': '#005314',
|
||||
'green-750': '#0D4510',
|
||||
'green-800': '#11380E',
|
||||
'green-850': '#132A11',
|
||||
'green-900': '#101E0F',
|
||||
'green-950': '#091209',
|
||||
'blue-1-050': '#EBF1F9',
|
||||
'blue-1-100': '#D6E4F4',
|
||||
'blue-1-150': '#C1D7F0',
|
||||
'blue-1-200': '#AACAEF',
|
||||
'blue-1-250': '#8FBCEF',
|
||||
'blue-1-300': '#7CAFEB',
|
||||
'blue-1-350': '#68A1E4',
|
||||
'blue-1-400': '#5B94D6',
|
||||
'blue-1-450': '#4E86C7',
|
||||
'blue-1-500': '#4279B9',
|
||||
'blue-1-550': '#356CAC',
|
||||
'blue-1-600': '#28609E',
|
||||
'blue-1-650': '#1B5390',
|
||||
'blue-1-700': '#0B4783',
|
||||
'blue-1-750': '#0F3C6E',
|
||||
'blue-1-800': '#133059',
|
||||
'blue-1-850': '#152641',
|
||||
'blue-1-900': '#121C2D',
|
||||
'blue-1-950': '#0B111C',
|
||||
'blue-2-050': '#E7F3F4',
|
||||
'blue-2-100': '#CEE7E9',
|
||||
'blue-2-150': '#B2DCE0',
|
||||
'blue-2-200': '#91D1D7',
|
||||
'blue-2-250': '#68C7D0',
|
||||
'blue-2-300': '#43BBC5',
|
||||
'blue-2-350': '#00AFBA',
|
||||
'blue-2-400': '#01A0AA',
|
||||
'blue-2-450': '#00929D',
|
||||
'blue-2-500': '#00848F',
|
||||
'blue-2-550': '#007682',
|
||||
'blue-2-600': '#016874',
|
||||
'blue-2-650': '#005B67',
|
||||
'blue-2-700': '#004E5A',
|
||||
'blue-2-750': '#00424E',
|
||||
'blue-2-800': '#003642',
|
||||
'blue-2-850': '#002A38',
|
||||
'blue-2-900': '#061E28',
|
||||
'blue-2-950': '#071219',
|
||||
'purple-050': '#F7F0F6',
|
||||
'purple-100': '#EEE0EE',
|
||||
'purple-150': '#E7D1E7',
|
||||
'purple-200': '#DBBFE4',
|
||||
'purple-250': '#D3AEE2',
|
||||
'purple-300': '#CB99E1',
|
||||
'purple-350': '#C188D9',
|
||||
'purple-400': '#B47BCB',
|
||||
'purple-450': '#A66EBD',
|
||||
'purple-500': '#9961AF',
|
||||
'purple-550': '#8B55A1',
|
||||
'purple-600': '#7E4894',
|
||||
'purple-650': '#723C87',
|
||||
'purple-700': '#633376',
|
||||
'purple-750': '#552A65',
|
||||
'purple-800': '#452551',
|
||||
'purple-850': '#35213D',
|
||||
'purple-900': '#261A2C',
|
||||
'purple-950': '#17111C',
|
||||
'pink-050': '#F8EFF4',
|
||||
'pink-100': '#F0DFEA',
|
||||
'pink-150': '#EACEDF',
|
||||
'pink-200': '#E9BBD1',
|
||||
'pink-250': '#E9A7C2',
|
||||
'pink-300': '#E095B4',
|
||||
'pink-350': '#D685A8',
|
||||
'pink-400': '#C7799B',
|
||||
'pink-450': '#B86C8D',
|
||||
'pink-500': '#AA5F80',
|
||||
'pink-550': '#9C5374',
|
||||
'pink-600': '#8E4767',
|
||||
'pink-650': '#813B5B',
|
||||
'pink-700': '#732E4F',
|
||||
'pink-750': '#632643',
|
||||
'pink-800': '#521F38',
|
||||
'pink-850': '#3E1C2B',
|
||||
'pink-900': '#2D171F',
|
||||
'pink-950': '#1C0E12',
|
||||
'black-000': '#1B1B2300',
|
||||
'black-050': '#1B1B230D',
|
||||
'black-100': '#1B1B231A',
|
||||
'black-150': '#1B1B2326',
|
||||
'black-200': '#1B1B2333',
|
||||
'black-250': '#1B1B2340',
|
||||
'black-300': '#1B1B234D',
|
||||
'black-350': '#1B1B2359',
|
||||
'black-400': '#1B1B2366',
|
||||
'black-450': '#1B1B2373',
|
||||
'black-500': '#1B1B2380',
|
||||
'black-550': '#1B1B238C',
|
||||
'black-600': '#1B1B2399',
|
||||
'black-650': '#1B1B23A6',
|
||||
'black-700': '#1B1B23B2',
|
||||
'black-750': '#1B1B23BF',
|
||||
'black-800': '#1B1B23CC',
|
||||
'black-850': '#1B1B23D9',
|
||||
'black-900': '#1B1B23E5',
|
||||
'black-950': '#111114F2',
|
||||
'white-000': '#F8F8F900',
|
||||
'white-050': '#F8F8F90D',
|
||||
'white-100': '#F8F8F91A',
|
||||
'white-150': '#F8F8F926',
|
||||
'white-200': '#F8F8F933',
|
||||
'white-250': '#F8F8F940',
|
||||
'white-300': '#F8F8F94D',
|
||||
'white-350': '#F8F8F959',
|
||||
'white-400': '#F8F8F966',
|
||||
'white-450': '#F8F8F973',
|
||||
'white-500': '#F8F8F980',
|
||||
'white-550': '#F8F8F98C',
|
||||
'white-600': '#F8F8F999',
|
||||
'white-650': '#F8F8F9A6',
|
||||
'white-700': '#F8F8F9B2',
|
||||
'white-750': '#F8F8F9BF',
|
||||
'white-800': '#F8F8F9CC',
|
||||
'white-850': '#F8F8F9D9',
|
||||
'white-900': '#F8F8F9E5',
|
||||
'white-950': '#F8F8F9F2',
|
||||
'white-975': '#F8F8F9F9',
|
||||
},
|
||||
...commonGlobals,
|
||||
font: {
|
||||
...commonGlobals.font,
|
||||
families: {
|
||||
base: 'Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif',
|
||||
accent: 'Hanken Grotesk, Inter, Roboto Flex Variable, sans-serif',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const dsfrGlobals = {
|
||||
...commonGlobals,
|
||||
components: {
|
||||
...commonGlobals.components,
|
||||
'la-gaufre': true,
|
||||
'home-proconnect': true,
|
||||
logo: {
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
'logo-1': '#2845C1',
|
||||
'logo-2': '#C83F49',
|
||||
'brand-050': '#EDF0FF',
|
||||
'brand-100': '#DAE2FF',
|
||||
'brand-150': '#C8D3FF',
|
||||
'brand-200': '#B5C4FF',
|
||||
'brand-250': '#A2B6FF',
|
||||
'brand-300': '#90A7FF',
|
||||
'brand-350': '#7E98FF',
|
||||
'brand-400': '#6C89FE',
|
||||
'brand-450': '#5C7AF7',
|
||||
'brand-500': '#4C6CEF',
|
||||
'brand-550': '#3E5DE7',
|
||||
'brand-600': '#304DDF',
|
||||
'brand-650': '#2845C1',
|
||||
'brand-700': '#223E9E',
|
||||
'brand-750': '#1F367D',
|
||||
'brand-800': '#1B2E5F',
|
||||
'brand-850': '#172446',
|
||||
'brand-900': '#121B30',
|
||||
'brand-950': '#0C111A',
|
||||
'gray-100': '#DFE2EA',
|
||||
'gray-150': '#CFD5DE',
|
||||
'gray-200': '#C1C7D3',
|
||||
'gray-250': '#B2B9C7',
|
||||
'gray-300': '#A4ABBC',
|
||||
'gray-350': '#969EB0',
|
||||
'gray-400': '#8891A4',
|
||||
'gray-450': '#7B8498',
|
||||
'gray-500': '#6D778C',
|
||||
'gray-550': '#626A80',
|
||||
'gray-600': '#555E74',
|
||||
'gray-650': '#4A5267',
|
||||
'gray-700': '#3F4759',
|
||||
'gray-750': '#363B4C',
|
||||
'gray-800': '#2B303D',
|
||||
'gray-850': '#222631',
|
||||
'gray-900': '#181B24',
|
||||
'gray-950': '#0F1117',
|
||||
'gray-1000': '#000000',
|
||||
'info-050': '#E7F2FF',
|
||||
'info-100': '#CFE5FF',
|
||||
'info-150': '#B7D7FF',
|
||||
'info-200': '#A0CAFE',
|
||||
'info-250': '#8CBCF9',
|
||||
'info-300': '#77AEF4',
|
||||
'info-350': '#63A0EE',
|
||||
'info-400': '#5092E7',
|
||||
'info-450': '#4185DC',
|
||||
'info-500': '#3677CC',
|
||||
'info-550': '#2F6ABB',
|
||||
'info-600': '#265EAA',
|
||||
'info-650': '#28528F',
|
||||
'info-700': '#274775',
|
||||
'info-750': '#243C5E',
|
||||
'info-800': '#20314A',
|
||||
'info-850': '#1B2637',
|
||||
'info-900': '#141C27',
|
||||
'info-950': '#0D1118',
|
||||
'success-050': '#DEF7E6',
|
||||
'success-100': '#BAEECF',
|
||||
'success-150': '#A5E2C0',
|
||||
'success-200': '#95D4B3',
|
||||
'success-250': '#85C6A7',
|
||||
'success-300': '#74B99B',
|
||||
'success-350': '#65AB8F',
|
||||
'success-400': '#579E84',
|
||||
'success-450': '#4B9079',
|
||||
'success-500': '#40836F',
|
||||
'success-550': '#367664',
|
||||
'success-600': '#2B695A',
|
||||
'success-650': '#2C5A50',
|
||||
'success-700': '#2A4D45',
|
||||
'success-750': '#26403C',
|
||||
'success-800': '#213430',
|
||||
'success-850': '#1B2826',
|
||||
'success-900': '#151D1C',
|
||||
'success-950': '#0D1212',
|
||||
'warning-050': '#FFEEDF',
|
||||
'warning-100': '#FFDCBE',
|
||||
'warning-150': '#FFCA9C',
|
||||
'warning-200': '#FFB778',
|
||||
'warning-250': '#FDA54F',
|
||||
'warning-300': '#F59425',
|
||||
'warning-350': '#E78613',
|
||||
'warning-400': '#D7790C',
|
||||
'warning-450': '#C86C08',
|
||||
'warning-500': '#B85F03',
|
||||
'warning-550': '#A75400',
|
||||
'warning-600': '#984800',
|
||||
'warning-650': '#814112',
|
||||
'warning-700': '#6C3A19',
|
||||
'warning-750': '#58321C',
|
||||
'warning-800': '#452A1A',
|
||||
'warning-850': '#352117',
|
||||
'warning-900': '#261813',
|
||||
'warning-950': '#170F0C',
|
||||
'error-050': '#FFEDEB',
|
||||
'error-100': '#FFDAD7',
|
||||
'error-150': '#FFC7C2',
|
||||
'error-200': '#FFB3AD',
|
||||
'error-250': '#FF9F99',
|
||||
'error-300': '#FF8984',
|
||||
'error-350': '#FF706E',
|
||||
'error-400': '#FB5759',
|
||||
'error-450': '#F63A45',
|
||||
'error-500': '#E32C39',
|
||||
'error-550': '#CF202D',
|
||||
'error-600': '#BD0F23',
|
||||
'error-650': '#9D2227',
|
||||
'error-700': '#812727',
|
||||
'error-750': '#672624',
|
||||
'error-800': '#512220',
|
||||
'error-850': '#3D1C1B',
|
||||
'error-900': '#2A1614',
|
||||
'error-950': '#190E0D',
|
||||
'red-050': '#FFEDEB',
|
||||
'red-100': '#FFDAD7',
|
||||
'red-150': '#FFC7C2',
|
||||
'red-200': '#FFB3AD',
|
||||
'red-250': '#FF9F99',
|
||||
'red-300': '#FF8984',
|
||||
'red-350': '#FF706E',
|
||||
'red-400': '#FB5759',
|
||||
'red-450': '#F63A45',
|
||||
'red-500': '#E32C39',
|
||||
'red-550': '#CF202D',
|
||||
'red-600': '#BD0F23',
|
||||
'red-650': '#9D2227',
|
||||
'red-700': '#812727',
|
||||
'red-750': '#672624',
|
||||
'red-800': '#512220',
|
||||
'red-850': '#3D1C1B',
|
||||
'red-900': '#410003',
|
||||
'red-950': '#190E0D',
|
||||
'orange-050': '#FCEDEB',
|
||||
'orange-100': '#F8DCD7',
|
||||
'orange-150': '#F1CCC5',
|
||||
'orange-200': '#EABCB4',
|
||||
'orange-250': '#E2ACA2',
|
||||
'orange-300': '#DA9C92',
|
||||
'orange-350': '#D28C81',
|
||||
'orange-400': '#CA7C70',
|
||||
'orange-450': '#BE6E62',
|
||||
'orange-500': '#AE6257',
|
||||
'orange-550': '#9E564D',
|
||||
'orange-600': '#8F4B42',
|
||||
'orange-650': '#79443D',
|
||||
'orange-700': '#643C37',
|
||||
'orange-750': '#513430',
|
||||
'orange-800': '#412B28',
|
||||
'orange-850': '#312220',
|
||||
'orange-900': '#231918',
|
||||
'orange-950': '#150F0F',
|
||||
'brown-050': '#F9EFEA',
|
||||
'brown-100': '#F3DFD3',
|
||||
'brown-150': '#EACFC1',
|
||||
'brown-200': '#E2BFAE',
|
||||
'brown-250': '#D8B19C',
|
||||
'brown-300': '#D0A189',
|
||||
'brown-350': '#C3937B',
|
||||
'brown-400': '#B5866D',
|
||||
'brown-450': '#A77A62',
|
||||
'brown-500': '#996D57',
|
||||
'brown-550': '#8B614D',
|
||||
'brown-600': '#7C5542',
|
||||
'brown-650': '#6A4C3C',
|
||||
'brown-700': '#594236',
|
||||
'brown-750': '#49382F',
|
||||
'brown-800': '#3B2E28',
|
||||
'brown-850': '#2D2420',
|
||||
'brown-900': '#201A18',
|
||||
'brown-950': '#13100F',
|
||||
'yellow-050': '#FDF1C5',
|
||||
'yellow-100': '#FBE18E',
|
||||
'yellow-150': '#F4D261',
|
||||
'yellow-200': '#EAC244',
|
||||
'yellow-250': '#DFB41B',
|
||||
'yellow-300': '#D1A516',
|
||||
'yellow-350': '#C49711',
|
||||
'yellow-400': '#B78A0C',
|
||||
'yellow-450': '#A87D07',
|
||||
'yellow-500': '#9B6F02',
|
||||
'yellow-550': '#8D6300',
|
||||
'yellow-600': '#7F5600',
|
||||
'yellow-650': '#6E4C11',
|
||||
'yellow-700': '#5D4219',
|
||||
'yellow-750': '#4D371B',
|
||||
'yellow-800': '#3D2E1A',
|
||||
'yellow-850': '#2F2417',
|
||||
'yellow-900': '#221A12',
|
||||
'yellow-950': '#14100C',
|
||||
'green-050': '#E7F9B3',
|
||||
'green-100': '#D5EC98',
|
||||
'green-150': '#C5DE86',
|
||||
'green-200': '#B5D174',
|
||||
'green-250': '#A5C464',
|
||||
'green-300': '#95B755',
|
||||
'green-350': '#85AA45',
|
||||
'green-400': '#769D39',
|
||||
'green-450': '#688F30',
|
||||
'green-500': '#5A8228',
|
||||
'green-550': '#4D7621',
|
||||
'green-600': '#416919',
|
||||
'green-650': '#3A5B20',
|
||||
'green-700': '#324E22',
|
||||
'green-750': '#2C4122',
|
||||
'green-800': '#24351D',
|
||||
'green-850': '#1D2919',
|
||||
'green-900': '#161E13',
|
||||
'green-950': '#0E120C',
|
||||
'blue-1-050': '#E7F2FF',
|
||||
'blue-1-100': '#CFE5FF',
|
||||
'blue-1-150': '#B7D7FF',
|
||||
'blue-1-200': '#A0CAFE',
|
||||
'blue-1-250': '#8CBCF9',
|
||||
'blue-1-300': '#77AEF4',
|
||||
'blue-1-350': '#63A0EE',
|
||||
'blue-1-400': '#5092E7',
|
||||
'blue-1-450': '#4185DC',
|
||||
'blue-1-500': '#3677CC',
|
||||
'blue-1-550': '#2F6ABB',
|
||||
'blue-1-600': '#265EAA',
|
||||
'blue-1-650': '#28528F',
|
||||
'blue-1-700': '#274775',
|
||||
'blue-1-750': '#243C5E',
|
||||
'blue-1-800': '#20314A',
|
||||
'blue-1-850': '#1B2637',
|
||||
'blue-1-900': '#141C27',
|
||||
'blue-1-950': '#0D1118',
|
||||
'blue-2-050': '#E2F4FD',
|
||||
'blue-2-100': '#C4E8F8',
|
||||
'blue-2-150': '#AADCF2',
|
||||
'blue-2-200': '#93CFEB',
|
||||
'blue-2-250': '#7CC2E2',
|
||||
'blue-2-300': '#6CB4D6',
|
||||
'blue-2-350': '#5CA7C9',
|
||||
'blue-2-400': '#5099BC',
|
||||
'blue-2-450': '#458BAE',
|
||||
'blue-2-500': '#3A7EA0',
|
||||
'blue-2-550': '#327191',
|
||||
'blue-2-600': '#286483',
|
||||
'blue-2-650': '#2B5770',
|
||||
'blue-2-700': '#294A5E',
|
||||
'blue-2-750': '#263E4D',
|
||||
'blue-2-800': '#22323D',
|
||||
'blue-2-850': '#1C272E',
|
||||
'blue-2-900': '#151D21',
|
||||
'blue-2-950': '#0E1114',
|
||||
'purple-050': '#F5EEFF',
|
||||
'purple-100': '#ECDCFF',
|
||||
'purple-150': '#E2CBFF',
|
||||
'purple-200': '#D9B9FF',
|
||||
'purple-250': '#D0A7FF',
|
||||
'purple-300': '#C894FE',
|
||||
'purple-350': '#BE83FA',
|
||||
'purple-400': '#B570F5',
|
||||
'purple-450': '#AB5EF0',
|
||||
'purple-500': '#A04BE8',
|
||||
'purple-550': '#933CDB',
|
||||
'purple-600': '#8530C8',
|
||||
'purple-650': '#7033A5',
|
||||
'purple-700': '#5D3185',
|
||||
'purple-750': '#4C2C6A',
|
||||
'purple-800': '#3C2652',
|
||||
'purple-850': '#2D203C',
|
||||
'purple-900': '#21182A',
|
||||
'purple-950': '#130F19',
|
||||
'pink-050': '#FFEBF6',
|
||||
'pink-100': '#FFD8ED',
|
||||
'pink-150': '#FCC4E3',
|
||||
'pink-200': '#F7B2D9',
|
||||
'pink-250': '#F29FCE',
|
||||
'pink-300': '#ED8CC3',
|
||||
'pink-350': '#E779B8',
|
||||
'pink-400': '#E264AD',
|
||||
'pink-450': '#D2579E',
|
||||
'pink-500': '#C24B8E',
|
||||
'pink-550': '#B0417F',
|
||||
'pink-600': '#9F3670',
|
||||
'pink-650': '#873560',
|
||||
'pink-700': '#6F3250',
|
||||
'pink-750': '#5A2C43',
|
||||
'pink-800': '#472635',
|
||||
'pink-850': '#351F29',
|
||||
'pink-900': '#26171D',
|
||||
'pink-950': '#170F12',
|
||||
'black-000': '#181B2400',
|
||||
'black-050': '#181B240D',
|
||||
'black-100': '#181B241A',
|
||||
'black-150': '#181B2426',
|
||||
'black-200': '#181B2433',
|
||||
'black-250': '#181B2440',
|
||||
'black-300': '#181B244D',
|
||||
'black-350': '#181B2459',
|
||||
'black-400': '#181B2466',
|
||||
'black-450': '#181B2473',
|
||||
'black-500': '#181B2480',
|
||||
'black-550': '#181B248C',
|
||||
'black-600': '#181B2499',
|
||||
'black-650': '#181B24A6',
|
||||
'black-700': '#181B24B2',
|
||||
'black-750': '#181B24BF',
|
||||
'black-800': '#181B24CC',
|
||||
'black-850': '#181B24D9',
|
||||
'black-900': '#181B24E5',
|
||||
'black-950': '#0F1117F2',
|
||||
'white-000': '#F6F8F900',
|
||||
'white-050': '#F6F8F90D',
|
||||
'white-100': '#F6F8F91A',
|
||||
'white-150': '#F6F8F926',
|
||||
'white-200': '#F6F8F933',
|
||||
'white-250': '#F6F8F940',
|
||||
'white-300': '#F6F8F94D',
|
||||
'white-350': '#F6F8F959',
|
||||
'white-400': '#F6F8F966',
|
||||
'white-450': '#F6F8F973',
|
||||
'white-500': '#F6F8F980',
|
||||
'white-550': '#F6F8F98C',
|
||||
'white-600': '#F6F8F999',
|
||||
'white-650': '#F6F8F9A6',
|
||||
'white-700': '#F6F8F9B2',
|
||||
'white-750': '#F6F8F9BF',
|
||||
'white-800': '#F6F8F9CC',
|
||||
'white-850': '#F6F8F9D9',
|
||||
'white-900': '#F6F8F9E5',
|
||||
'white-950': '#F6F8F9F2',
|
||||
'white-975': '#F6F8F9F9',
|
||||
},
|
||||
};
|
||||
|
||||
const whiteLabelThemes = getThemesFromGlobals(whiteLabelGlobals, {
|
||||
overrides: commonTokenOverrides,
|
||||
});
|
||||
const dsfrThemes = getThemesFromGlobals(dsfrGlobals, {
|
||||
overrides: commonTokenOverrides,
|
||||
});
|
||||
|
||||
if (dsfrThemes.dark) {
|
||||
dsfrThemes.dark.globals.components.logo.src =
|
||||
'/assets/logo-gouv-darkmode.svg';
|
||||
dsfrThemes.dark.globals.colors['logo-1'] = '#95ABFF';
|
||||
dsfrThemes.dark.globals.colors['logo-2'] = '#E78087';
|
||||
}
|
||||
|
||||
console.log(dsfrThemes);
|
||||
|
||||
const themes = {
|
||||
themes: {
|
||||
default: whiteLabelThemes.light,
|
||||
dark: whiteLabelThemes.dark,
|
||||
dsfr: dsfrThemes.light,
|
||||
'dsfr-dark': dsfrThemes.dark,
|
||||
},
|
||||
};
|
||||
|
||||
const config = { ...themes };
|
||||
|
||||
export default config;
|
||||
|
||||
Vendored
-1
@@ -1,6 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -41,7 +41,6 @@ const nextConfig = {
|
||||
|
||||
return config;
|
||||
},
|
||||
turbopack: {},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user