Give messages an icon, a bold title and soft styling
Each level now renders as a daisyUI soft alert: an icon, a bold heading and the
message text.
Django messages carry a level and a string -- there is no title field -- so the
heading comes from the level ("Done", "Careful", "Something went wrong"), and a
call site that wants a specific one passes it as extra_tags:
messages.success(request, f"{club} is live.", extra_tags="Club created")
The lookup is keyed on level_tag, not tags. `tags` is extra_tags and level_tag
joined, so the old `message.tags == "error"` test would have stopped matching the
moment any message carried a custom title, and every alert would have quietly
rendered as blue info.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,36 @@ WIDGET_CLASSES = (
|
||||
DEFAULT_WIDGET_CLASS = "input input-bordered w-full"
|
||||
|
||||
|
||||
#: Icon, default heading and daisyUI colour per message level.
|
||||
MESSAGE_ALERTS = {
|
||||
"debug": ("bug", "Debug", "alert-info"),
|
||||
"info": ("info", "Heads up", "alert-info"),
|
||||
"success": ("circle-check", "Done", "alert-success"),
|
||||
"warning": ("triangle-alert", "Careful", "alert-warning"),
|
||||
"error": ("circle-x", "Something went wrong", "alert-error"),
|
||||
}
|
||||
DEFAULT_MESSAGE_ALERT = MESSAGE_ALERTS["info"]
|
||||
|
||||
|
||||
@register.filter
|
||||
def as_alert(message):
|
||||
"""Presentation for one Django message: icon, bold title, body, colour.
|
||||
|
||||
Django messages carry a level and a string — there is no title field — so the
|
||||
title comes from the level, and a call site that wants a specific one passes it
|
||||
as ``extra_tags``::
|
||||
|
||||
messages.success(request, f"{club} is live.", extra_tags="Club created")
|
||||
|
||||
Keyed on ``level_tag``, never ``tags``: ``tags`` is extra_tags and level_tag
|
||||
joined, so a message carrying a custom title would stop matching its own level
|
||||
and quietly render as info.
|
||||
"""
|
||||
icon, title, css = MESSAGE_ALERTS.get(message.level_tag, DEFAULT_MESSAGE_ALERT)
|
||||
|
||||
return {"icon": icon, "title": message.extra_tags or title, "body": message.message, "css": css}
|
||||
|
||||
|
||||
@register.filter
|
||||
def daisy(field):
|
||||
"""Render a bound form field with the right daisyUI classes."""
|
||||
|
||||
@@ -2,7 +2,9 @@ from decimal import Decimal
|
||||
|
||||
from allauth.mfa.models import Authenticator
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.messages.storage.base import Message
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
@@ -17,7 +19,7 @@ from teams.models import Position, Team, TeamMembership
|
||||
from .services.admins import grant_club_admin
|
||||
from .services.platform_admins import PlatformAdminError, is_last_superuser, set_platform_access
|
||||
from .services.statistics import club_statistics, clubs_with_totals, platform_totals
|
||||
from .templatetags.ui import daisy
|
||||
from .templatetags.ui import as_alert, daisy
|
||||
|
||||
User = get_user_model()
|
||||
Flag = get_waffle_flag_model()
|
||||
@@ -423,3 +425,33 @@ class FeatureViewTests(ControlPanelTestBase):
|
||||
|
||||
self.assertContains(response, "On for all clubs")
|
||||
self.assertNotContains(response, reverse("controlpanel:club_feature_toggle", args=[self.club.pk, self.flag.pk]))
|
||||
|
||||
|
||||
class MessageAlertTests(TestCase):
|
||||
def alert(self, level, text, extra_tags=None):
|
||||
return as_alert(Message(level, text, extra_tags=extra_tags))
|
||||
|
||||
def test_each_level_gets_its_own_icon_title_and_colour(self):
|
||||
self.assertEqual(self.alert(messages.SUCCESS, "Saved.")["icon"], "circle-check")
|
||||
self.assertEqual(self.alert(messages.WARNING, "Careful.")["css"], "alert-warning")
|
||||
self.assertEqual(self.alert(messages.ERROR, "Boom.")["title"], "Something went wrong")
|
||||
self.assertEqual(self.alert(messages.INFO, "FYI.")["css"], "alert-info")
|
||||
|
||||
def test_extra_tags_override_the_title(self):
|
||||
alert = self.alert(messages.SUCCESS, "Ajax United is live.", extra_tags="Club created")
|
||||
|
||||
self.assertEqual(alert["title"], "Club created")
|
||||
self.assertEqual(alert["body"], "Ajax United is live.")
|
||||
self.assertEqual(alert["css"], "alert-success") # a custom title must not change the level
|
||||
|
||||
def test_an_unknown_level_falls_back_to_info(self):
|
||||
self.assertEqual(self.alert(999, "Odd.")["css"], "alert-info")
|
||||
|
||||
|
||||
class MessageRenderingTests(ControlPanelTestBase):
|
||||
def test_a_message_renders_as_a_soft_alert_with_icon_and_title(self):
|
||||
response = self.client.post(reverse("controlpanel:club_archive", args=[self.club.pk]), follow=True)
|
||||
|
||||
self.assertContains(response, "alert alert-soft alert-warning")
|
||||
self.assertContains(response, '<div class="font-bold">Careful</div>', html=False)
|
||||
self.assertContains(response, "<svg") # the lucide icon
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{% load lucide static %}
|
||||
{% load lucide static ui %}
|
||||
|
||||
{% comment %}
|
||||
The page skeleton, with no branding of its own. `_platform_base.html` dresses it
|
||||
@@ -68,9 +68,15 @@
|
||||
{% if messages %}
|
||||
<div class="mx-auto mt-4 w-full space-y-2 px-4">
|
||||
{% for message in messages %}
|
||||
<div class="alert {% if message.tags == 'error' %}alert-error{% elif message.tags == 'warning' %}alert-warning{% elif message.tags == 'success' %}alert-success{% else %}alert-info{% endif %}">
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
{% with alert=message|as_alert %}
|
||||
<div class="alert alert-soft {{ alert.css }}" role="alert">
|
||||
{% lucide alert.icon size=20 %}
|
||||
<div>
|
||||
<div class="font-bold">{{ alert.title }}</div>
|
||||
<div class="text-sm">{{ alert.body }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user