[CHANGE] Beautify Admin Announcements

This commit is contained in:
Peter Pfeufer committed 2026-08-20 17:42:43 +02:00
1 parent 78494a28a7
commit ef2f12fa7d
3 files changed
+112 -29

No files matched your search

@@ -46,36 +46,41 @@ the escapejs filter without having to redefine them later.
{% translate "Announcements" as widget_title %}
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
<div>
<ul class="list-group">
{% for notif in notifications %}
{% if not notif.is_hidden %}
<li class="list-group-item">
<span class="badge text-bg-info me-2">{{ notif.application_name }}</span>
<a href="{{ notif.announcement_url }}" target="_blank" rel="noopener noreferrer">{{ notif.announcement_text }}</a>
</li>
{% endif %}
{% empty %}
<div class="alert alert-primary" role="alert">
{% translate "No notifications at this time" %}
<div class="row">
{% with apps_count=notifications|length %}
{% for application in notifications %}
<div class="{% if apps_count >= 2 %}col-md-6{% endif %} {% if apps_count >= 3 %}col-lg-4{% endif %}">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{{ application.application_name }}</h5>
<ul class="list-group list-group-flush">
{% for announcement in application.announcements %}
<li class="list-group-item">
<a href="{{ announcement.announcement_url }}" target="_blank" rel="noopener noreferrer">{{ announcement.announcement_text }}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endfor %}
</ul>
{% endwith %}
</div>
<div class="text-end pt-3">
<a href="https://gitlab.com/allianceauth/allianceauth/issues" target="_blank" class="me-1 text-decoration-none">
<span class="badge text-bg-danger">
<i class="fab fa-gitlab" aria-hidden="true"></i>
{% translate 'Powered by GitLab' %}
</span>
</a>
<a href="https://discord.com/invite/fjnHAmk" target="_blank" class="text-decoration-none">
<span class="badge text-bg-info">
<i class="fab fa-discord" aria-hidden="true"></i>
{% translate 'Support Discord' %}
</span>
</a>
</div>
<div class="text-end pt-3">
<a href="https://gitlab.com/allianceauth/allianceauth/issues" target="_blank" class="me-1 text-decoration-none">
<span class="badge text-bg-danger">
<i class="fab fa-gitlab" aria-hidden="true"></i>
{% translate 'Powered by GitLab' %}
</span>
</a>
<a href="https://discord.com/invite/fjnHAmk" target="_blank" class="text-decoration-none">
<span class="badge text-bg-info">
<i class="fab fa-discord" aria-hidden="true"></i>
{% translate 'Support Discord' %}
</span>
</a>
</div>
</div>
</div>
@@ -72,7 +72,7 @@ def _current_notifications() -> dict:
# Ensure "Alliance Auth" appears first, then the rest alphabetically.
# We annotate a custom ordering field: 0 for Alliance Auth, 1 for others.
application_notifications = (
application_announcements_qs = (
ApplicationAnnouncement.objects.annotate(
order_priority=Case(
When(application_name="Alliance Auth", then=Value(0)),
@@ -83,9 +83,43 @@ def _current_notifications() -> dict:
.order_by("order_priority", "application_name", "-announcement_number")
)
# change the dict format to a list of dicts for easier template rendering
# `application_notifications = [{"application_name": "", "announcements": []}]`
# Build groups in Python to ensure we preserve the queryset order and avoid
# duplicates regardless of DB distinct behavior. This also avoids extra
# per-application queries by iterating the queryset once.
grouped = {}
ordered_names = []
for announcement in application_announcements_qs:
name = announcement.application_name
if name not in grouped:
grouped[name] = []
ordered_names.append(name)
grouped[name].append(announcement)
application_notifications = [
{
"application_name": app_name,
"announcements": [
{
"announcement_number": announcement.announcement_number,
"announcement_text": announcement.announcement_text,
"announcement_url": announcement.announcement_url,
"hide_announcement": announcement.hide_announcement,
}
for announcement in grouped.get(app_name, [])
],
}
for app_name in ordered_names
]
response = {
"notifications": application_notifications,
}
return response
@@ -228,8 +228,23 @@ class TestCurrentNotifications(NoSocketsTestCase):
# The implementation calls .objects.annotate(...).order_by(...)
mock_model.objects.annotate.return_value.order_by.return_value = mock_list
result = admin_status._current_notifications()
expected_notifications = [
{
"application_name": mock_ann.application_name,
"announcements": [
{
"announcement_number": mock_ann.announcement_number,
"announcement_text": mock_ann.announcement_text,
"announcement_url": mock_ann.announcement_url,
"hide_announcement": mock_ann.hide_announcement,
}
],
}
]
self.assertIn("notifications", result)
self.assertEqual(result["notifications"], mock_list)
self.assertEqual(result["notifications"], expected_notifications)
def test_returns_empty_notifications_when_none_saved(self):
with patch(MODULE_PATH + ".ApplicationAnnouncement") as mock_model:
@@ -238,6 +253,35 @@ class TestCurrentNotifications(NoSocketsTestCase):
self.assertIn("notifications", result)
self.assertEqual(list(result["notifications"]), [])
def test_if_name_not_in_grouped_is_false_on_second_iteration(self):
ann1 = MagicMock()
ann1.application_name = "Same App"
ann1.announcement_number = 5
ann1.announcement_text = "First"
ann2 = MagicMock()
ann2.application_name = "Same App"
ann2.announcement_number = 4
ann2.announcement_text = "Second"
with patch(MODULE_PATH + ".ApplicationAnnouncement") as mock_model:
mock_model.objects.annotate.return_value.order_by.return_value = [
ann1,
ann2,
]
result = admin_status._current_notifications()
self.assertIn("notifications", result)
notifications = result["notifications"]
# the grouped logic should produce a single application entry (if condition false on second)
self.assertEqual(len(notifications), 1)
self.assertEqual(notifications[0]["application_name"], "Same App")
# and both announcements should be present under that single application
self.assertEqual(len(notifications[0]["announcements"]), 2)
self.assertEqual(
[a["announcement_number"] for a in notifications[0]["announcements"]],
[5, 4],
)
class TestFetchTagsFromGitlab(NoSocketsTestCase):
@requests_mock.mock()