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")