diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f4fb9a..c0544d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ and this project adheres to
### Added
+- ✨(backend) add admin action to check domain health
- ✨(dimail) check domain health
- ✨(frontend) disable mailbox and allow to create pending mailbox
- ✨(organizations) add siret to name conversion #584
diff --git a/src/backend/admin/templates/admin/base_site.html b/src/backend/admin/templates/admin/base_site.html
index 8139baf..5260f10 100644
--- a/src/backend/admin/templates/admin/base_site.html
+++ b/src/backend/admin/templates/admin/base_site.html
@@ -6,5 +6,8 @@ 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;
+}
{% endblock %}
diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.mo b/src/backend/locale/fr_FR/LC_MESSAGES/django.mo
index 4e0fe3e..a5b7ee5 100644
Binary files a/src/backend/locale/fr_FR/LC_MESSAGES/django.mo and b/src/backend/locale/fr_FR/LC_MESSAGES/django.mo differ
diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.po b/src/backend/locale/fr_FR/LC_MESSAGES/django.po
index 8c9bc5b..b20e5ea 100644
--- a/src/backend/locale/fr_FR/LC_MESSAGES/django.po
+++ b/src/backend/locale/fr_FR/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-people\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-11-18 23:02+0000\n"
+"POT-Creation-Date: 2025-01-09 11:42+0000\n"
"PO-Revision-Date: 2024-01-03 23:15\n"
"Last-Translator: \n"
"Language-Team: French\n"
@@ -17,35 +17,35 @@ msgstr ""
"X-Crowdin-File: backend.pot\n"
"X-Crowdin-File-ID: 2\n"
-#: core/admin.py:55
+#: core/admin.py:56
msgid "Personal info"
msgstr ""
-#: core/admin.py:57
+#: core/admin.py:58
msgid "Permissions"
msgstr ""
-#: core/admin.py:69
+#: core/admin.py:70
msgid "Important dates"
msgstr ""
-#: core/admin.py:108
+#: core/admin.py:109
msgid "User"
msgstr ""
-#: core/authentication/backends.py:89
+#: core/authentication/backends.py:94
msgid "User info contained no recognizable user identification"
msgstr ""
-#: core/authentication/backends.py:111
+#: core/authentication/backends.py:116
msgid "User account is disabled"
msgstr ""
-#: core/authentication/backends.py:157
+#: core/authentication/backends.py:162
msgid "Claims contained no recognizable user identification"
msgstr ""
-#: core/authentication/backends.py:176
+#: core/authentication/backends.py:181
msgid "Claims contained no recognizable organization identification"
msgstr ""
@@ -97,181 +97,198 @@ msgstr ""
msgid "date and time at which a record was last updated"
msgstr ""
-#: core/models.py:116
+#: core/models.py:124
msgid "full name"
msgstr ""
-#: core/models.py:117
+#: core/models.py:125
msgid "short name"
msgstr ""
-#: core/models.py:122
+#: core/models.py:128
+msgid "notes"
+msgstr ""
+
+#: core/models.py:130
msgid "contact information"
msgstr ""
-#: core/models.py:123
+#: core/models.py:131
msgid "A JSON object containing the contact information"
msgstr ""
-#: core/models.py:137
+#: core/models.py:145
msgid "contact"
msgstr ""
-#: core/models.py:138
+#: core/models.py:146
msgid "contacts"
msgstr ""
-#: core/models.py:262 core/models.py:331 mailbox_manager/models.py:24
+#: core/models.py:220 core/models.py:318 core/models.py:440
+#: mailbox_manager/models.py:24
msgid "name"
msgstr ""
-#: core/models.py:270
+#: core/models.py:222
+msgid "audience id"
+msgstr ""
+
+#: core/models.py:227
+msgid "service provider"
+msgstr ""
+
+#: core/models.py:228
+msgid "service providers"
+msgstr ""
+
+#: core/models.py:326
msgid "registration ID list"
msgstr ""
-#: core/models.py:279
+#: core/models.py:333
msgid "domain list"
msgstr ""
-#: core/models.py:289
+#: core/models.py:349
msgid "organization"
msgstr ""
-#: core/models.py:290
+#: core/models.py:350
msgid "organizations"
msgstr ""
-#: core/models.py:297
+#: core/models.py:357
msgid "An organization must have at least a registration ID or a domain."
msgstr ""
-#: core/models.py:316
+#: core/models.py:425
msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters."
msgstr ""
-#: core/models.py:322
+#: core/models.py:431
msgid "sub"
msgstr ""
-#: core/models.py:324
+#: core/models.py:433
msgid ""
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
"characters only."
msgstr ""
-#: core/models.py:330 core/models.py:737
+#: core/models.py:439 core/models.py:844
msgid "email address"
msgstr ""
-#: core/models.py:343
+#: core/models.py:445
msgid "language"
msgstr ""
-#: core/models.py:344
+#: core/models.py:446
msgid "The language in which the user wants to see the interface."
msgstr ""
-#: core/models.py:350
+#: core/models.py:452
msgid "The timezone in which the user wants to see times."
msgstr ""
-#: core/models.py:353
+#: core/models.py:455
msgid "device"
msgstr ""
-#: core/models.py:355
+#: core/models.py:457
msgid "Whether the user is a device or a real user."
msgstr ""
-#: core/models.py:358
+#: core/models.py:460
msgid "staff status"
msgstr ""
-#: core/models.py:360
+#: core/models.py:462
msgid "Whether the user can log into this admin site."
msgstr ""
-#: core/models.py:363
+#: core/models.py:465
msgid "active"
msgstr ""
-#: core/models.py:366
+#: core/models.py:468
msgid ""
"Whether this user should be treated as active. Unselect this instead of "
"deleting accounts."
msgstr ""
-#: core/models.py:385
+#: core/models.py:487
msgid "user"
msgstr ""
-#: core/models.py:386
+#: core/models.py:488
msgid "users"
msgstr ""
-#: core/models.py:525
+#: core/models.py:622
msgid "Organization/user relation"
msgstr ""
-#: core/models.py:526
+#: core/models.py:623
msgid "Organization/user relations"
msgstr ""
-#: core/models.py:531
+#: core/models.py:628
msgid "This user is already in this organization."
msgstr ""
-#: core/models.py:563
+#: core/models.py:670
msgid "Team"
msgstr ""
-#: core/models.py:564
+#: core/models.py:671
msgid "Teams"
msgstr ""
-#: core/models.py:615
+#: core/models.py:722
msgid "Team/user relation"
msgstr ""
-#: core/models.py:616
+#: core/models.py:723
msgid "Team/user relations"
msgstr ""
-#: core/models.py:621
+#: core/models.py:728
msgid "This user is already in this team."
msgstr ""
-#: core/models.py:710
+#: core/models.py:817
msgid "url"
msgstr ""
-#: core/models.py:711
+#: core/models.py:818
msgid "secret"
msgstr ""
-#: core/models.py:720
+#: core/models.py:827
msgid "Team webhook"
msgstr ""
-#: core/models.py:721
+#: core/models.py:828
msgid "Team webhooks"
msgstr ""
-#: core/models.py:754
+#: core/models.py:861
msgid "Team invitation"
msgstr ""
-#: core/models.py:755
+#: core/models.py:862
msgid "Team invitations"
msgstr ""
-#: core/models.py:780
+#: core/models.py:887
msgid "This email is already associated to a registered user."
msgstr ""
-#: core/models.py:822 core/models.py:828
+#: core/models.py:929 core/models.py:935
msgid "Invitation to join Desk!"
msgstr ""
@@ -456,20 +473,51 @@ msgstr "L'équipe de La Suite"
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
msgstr ""
-#: mailbox_manager/admin.py:12
+#: mailbox_manager/admin.py:13
msgid "Synchronise from dimail"
msgstr ""
-#: mailbox_manager/admin.py:23
+#: mailbox_manager/admin.py:24
#, python-brace-format
msgid "Synchronisation failed for {domain.name} with message: [{err}]"
msgstr ""
-#: mailbox_manager/admin.py:29
+#: mailbox_manager/admin.py:30
#, python-brace-format
msgid "Synchronisation succeed for {domain.name}. "
msgstr ""
+#: mailbox_manager/admin.py:36
+msgid "Check and update status from dimail"
+msgstr "Vérification et mise à jour de l'état à partir de dimail"
+
+#: mailbox_manager/admin.py:52
+#, python-brace-format
+msgid "- {domain.name} with message: '{err}'"
+msgstr "- {domain.name} avec le message: '{err}'"
+
+#: mailbox_manager/admin.py:63
+msgid "Check domains done with success."
+msgstr "Vérification des domains réalisée avec success."
+
+#: mailbox_manager/admin.py:64
+msgid "Domains updated: {', '.join(domains_updated)}"
+msgstr "Domaines mis à jour : {', '.join(domains_updated)}"
+
+#: mailbox_manager/admin.py:66
+msgid "No domain updated."
+msgstr "Aucun domain n'a été mis à jour."
+
+#: mailbox_manager/admin.py:70
+msgid "Check domain failed for:"
+msgstr "La vérification de domain a échoué pour :"
+
+#: mailbox_manager/admin.py:76
+msgid "Domains disabled are excluded from check: {', '.join(excluded_domains)}"
+msgstr ""
+"Les domains désactivés sont exclus de la vérification : {', '."
+"join(excluded_domains)}"
+
#: mailbox_manager/enums.py:13
msgid "Viewer"
msgstr ""
@@ -520,16 +568,17 @@ msgstr ""
#: mailbox_manager/models.py:224
msgid "You can't create or update a mailbox for a disabled domain."
-msgstr "Vous ne pouvez pas créer ou modifier une boîte mail pour un domain désactivé."
+msgstr ""
+"Vous ne pouvez pas créer ou modifier une boîte mail pour un domain désactivé."
-#: mailbox_manager/utils/dimail.py:183
+#: mailbox_manager/utils/dimail.py:262
msgid "Your new mailbox information"
msgstr "Informations concernant votre nouvelle boîte mail"
-#: people/settings.py:135
+#: people/settings.py:146
msgid "English"
msgstr ""
-#: people/settings.py:136
+#: people/settings.py:147
msgid "French"
msgstr ""
diff --git a/src/backend/mailbox_manager/admin.py b/src/backend/mailbox_manager/admin.py
index 2a3d0a5..d17892b 100644
--- a/src/backend/mailbox_manager/admin.py
+++ b/src/backend/mailbox_manager/admin.py
@@ -1,11 +1,12 @@
"""Admin classes and registrations for People's mailbox manager app."""
from django.contrib import admin, messages
+from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from requests import exceptions
-from mailbox_manager import models
+from mailbox_manager import enums, models
from mailbox_manager.utils.dimail import DimailAPIClient
@@ -32,6 +33,51 @@ def sync_mailboxes_from_dimail(modeladmin, request, queryset): # pylint: disabl
)
+@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."""
+ client = DimailAPIClient()
+ domains_updated, excluded_domains, msg_error = [], [], []
+ success = False
+ for domain in queryset:
+ # do not check disabled domains
+ if domain.status == enums.MailDomainStatusChoices.DISABLED:
+ excluded_domains.append(domain.name)
+ continue
+
+ old_status = domain.status
+ try:
+ response = client.fetch_domain_status(domain)
+ except exceptions.HTTPError as err:
+ msg_error.append(_(f"""- {domain.name} with message: '{err}'"""))
+ else:
+ success = True
+ # temporary (or not?) display content of the dimail response to debug broken state
+ if domain.status == enums.MailDomainStatusChoices.FAILED:
+ messages.info(request, response.json())
+ if old_status != domain.status:
+ domains_updated.append(domain.name)
+
+ if success:
+ msg_success = [
+ _("Check domains done with success."),
+ _(f"Domains updated: {', '.join(domains_updated)}")
+ if domains_updated
+ else _("No domain updated."),
+ ]
+ messages.success(request, format_html("
".join(map(str, msg_success))))
+ if msg_error:
+ msg_error.insert(0, _("Check domain failed for:"))
+ messages.error(request, format_html("
".join(map(str, msg_error))))
+ if excluded_domains:
+ messages.warning(
+ request,
+ _(
+ f"Domains disabled are excluded from check: {', '.join(excluded_domains)}"
+ ),
+ )
+
+
class UserMailDomainAccessInline(admin.TabularInline):
"""Inline admin class for mail domain accesses."""
@@ -54,7 +100,7 @@ class MailDomainAdmin(admin.ModelAdmin):
search_fields = ("name",)
readonly_fields = ["created_at", "slug"]
inlines = (UserMailDomainAccessInline,)
- actions = (sync_mailboxes_from_dimail,)
+ actions = (sync_mailboxes_from_dimail, fetch_domain_status_from_dimail)
@admin.register(models.Mailbox)
diff --git a/src/backend/mailbox_manager/tests/test_admin_actions.py b/src/backend/mailbox_manager/tests/test_admin_actions.py
new file mode 100644
index 0000000..61f440c
--- /dev/null
+++ b/src/backend/mailbox_manager/tests/test_admin_actions.py
@@ -0,0 +1,78 @@
+"""
+Unit tests for admin actions
+"""
+
+import json
+import re
+
+from django.urls import reverse
+
+import pytest
+import responses
+
+from core import factories as core_factories
+
+from mailbox_manager import enums, factories
+
+from .fixtures.dimail import CHECK_DOMAIN_BROKEN
+
+
+@pytest.mark.django_db
+def test_admin_action__fetch_domain_status_from_dimail(client):
+ """Test admin action to check health of some domains"""
+ admin = core_factories.UserFactory(is_staff=True, is_superuser=True)
+ client.force_login(admin)
+ domain1 = factories.MailDomainEnabledFactory()
+ domain2 = factories.MailDomainEnabledFactory()
+ data = {
+ "action": "fetch_domain_status_from_dimail",
+ "_selected_action": [
+ domain1.id,
+ domain2.id,
+ ],
+ }
+ url = reverse("admin:mailbox_manager_maildomain_changelist")
+
+ with responses.RequestsMock() as rsps:
+ body_content_domain1 = CHECK_DOMAIN_BROKEN.copy()
+ body_content_domain1["name"] = domain1.name
+ body_content_domain2 = CHECK_DOMAIN_BROKEN.copy()
+ body_content_domain2["name"] = domain2.name
+ rsps.add(
+ rsps.GET,
+ re.compile(rf".*/domains/{domain1.name}/check/"),
+ body=json.dumps(body_content_domain1),
+ status=200,
+ content_type="application/json",
+ )
+ rsps.add(
+ rsps.GET,
+ re.compile(rf".*/domains/{domain2.name}/check/"),
+ body=json.dumps(body_content_domain2),
+ status=200,
+ content_type="application/json",
+ )
+ response = client.post(url, data, follow=True)
+ assert response.status_code == 200
+ domain1.refresh_from_db()
+ domain2.refresh_from_db()
+ assert domain1.status == enums.MailDomainStatusChoices.FAILED
+ assert domain2.status == enums.MailDomainStatusChoices.FAILED
+ assert "Check domains done with success" in response.content.decode("utf-8")
+
+ # check with a valid domain info from dimail
+ body_content_domain1["state"] = "ok"
+ rsps.add(
+ rsps.GET,
+ re.compile(rf".*/domains/{domain1.name}/check/"),
+ body=json.dumps(body_content_domain1),
+ status=200,
+ content_type="application/json",
+ )
+ response = client.post(url, data, follow=True)
+ assert response.status_code == 200
+ domain1.refresh_from_db()
+ domain2.refresh_from_db()
+ assert domain1.status == enums.MailDomainStatusChoices.ENABLED
+ assert domain2.status == enums.MailDomainStatusChoices.FAILED
+ assert "Check domains done with success" in response.content.decode("utf-8")