feat(extras): Add "Dismiss all" action to notifications dropdown

Introduce a view to allow users to dismiss all unread notifications with
a single action. Update the notifications' template to include a
"Dismiss all" button for enhanced usability. This addition streamlines
notification management and improves the user experience.

Fixes #20301
This commit is contained in:
Martin Hauser 2025-10-22 13:59:54 +02:00
parent cb3308a166
commit 8eaff9dce7
No known key found for this signature in database
3 changed files with 94 additions and 3 deletions

View File

@ -4,7 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.paginator import EmptyPage from django.core.paginator import EmptyPage
from django.db.models import Count, Q from django.db.models import Count, Q
from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse, Http404
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -25,7 +25,7 @@ from netbox.object_actions import *
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
from utilities.htmx import htmx_partial from utilities.htmx import htmx_partial, htmx_maybe_redirect_current_page
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.query import count_related from utilities.query import count_related
from utilities.querydict import normalize_querydict from utilities.querydict import normalize_querydict
@ -518,8 +518,9 @@ class NotificationsView(LoginRequiredMixin, View):
""" """
def get(self, request): def get(self, request):
return render(request, 'htmx/notifications.html', { return render(request, 'htmx/notifications.html', {
'notifications': request.user.notifications.unread(), 'notifications': request.user.notifications.unread()[:10],
'total_count': request.user.notifications.count(), 'total_count': request.user.notifications.count(),
'unread_count': request.user.notifications.unread().count(),
}) })
@ -528,6 +529,7 @@ class NotificationReadView(LoginRequiredMixin, View):
""" """
Mark the Notification read and redirect the user to its attached object. Mark the Notification read and redirect the user to its attached object.
""" """
def get(self, request, pk): def get(self, request, pk):
# Mark the Notification as read # Mark the Notification as read
notification = get_object_or_404(request.user.notifications, pk=pk) notification = get_object_or_404(request.user.notifications, pk=pk)
@ -541,18 +543,48 @@ class NotificationReadView(LoginRequiredMixin, View):
return redirect('account:notifications') return redirect('account:notifications')
@register_model_view(Notification, name='dismiss_all', path='dismiss-all', detail=False)
class NotificationDismissAllView(LoginRequiredMixin, View):
"""
Convenience view to clear all *unread* notifications for the current user.
"""
def get(self, request):
request.user.notifications.unread().delete()
if htmx_partial(request):
# If a user is currently on the notification page, redirect there (full repaint)
redirect_resp = htmx_maybe_redirect_current_page(request, 'account:notifications', preserve_query=True)
if redirect_resp:
return redirect_resp
return render(request, 'htmx/notifications.html', {
'notifications': request.user.notifications.unread()[:10],
'total_count': request.user.notifications.count(),
'unread_count': request.user.notifications.unread().count(),
})
return redirect('account:notifications')
@register_model_view(Notification, 'dismiss') @register_model_view(Notification, 'dismiss')
class NotificationDismissView(LoginRequiredMixin, View): class NotificationDismissView(LoginRequiredMixin, View):
""" """
A convenience view which allows deleting notifications with one click. A convenience view which allows deleting notifications with one click.
""" """
def get(self, request, pk): def get(self, request, pk):
notification = get_object_or_404(request.user.notifications, pk=pk) notification = get_object_or_404(request.user.notifications, pk=pk)
notification.delete() notification.delete()
if htmx_partial(request): if htmx_partial(request):
# If a user is currently on the notification page, redirect there (full repaint)
redirect_resp = htmx_maybe_redirect_current_page(request, 'account:notifications', preserve_query=True)
if redirect_resp:
return redirect_resp
return render(request, 'htmx/notifications.html', { return render(request, 'htmx/notifications.html', {
'notifications': request.user.notifications.unread()[:10], 'notifications': request.user.notifications.unread()[:10],
'total_count': request.user.notifications.count(),
'unread_count': request.user.notifications.unread().count(),
}) })
return redirect('account:notifications') return redirect('account:notifications')

View File

@ -1,4 +1,15 @@
{% load i18n %} {% load i18n %}
<div class="card-header px-2 py-1">
<h3 class="card-title flex-fill">Notifications</h3>
{% if notifications %}
<a href="#" hx-get="{% url 'extras:notification_dismiss_all' %}" hx-target="closest .notifications"
hx-confirm="{% blocktrans trimmed count count=unread_count %}Dismiss {{ count }} unread notification?{% plural %}Dismiss {{ count }} unread notifications?{% endblocktrans %}"
class="btn btn-2 text-danger" title="{% trans 'Dismiss all unread notifications' %}">
<i class="icon mdi mdi-delete-sweep-outline"></i>
{% trans "Dismiss all" %}
</a>
{% endif %}
</div>
<div class="list-group list-group-flush list-group-hoverable" style="min-width: 300px"> <div class="list-group list-group-flush list-group-hoverable" style="min-width: 300px">
{% for notification in notifications %} {% for notification in notifications %}
<div class="list-group-item p-2"> <div class="list-group-item p-2">

View File

@ -1,5 +1,11 @@
from django.http import HttpResponse
from django.urls import reverse
from urllib.parse import urlsplit
__all__ = ( __all__ = (
'htmx_current_url',
'htmx_partial', 'htmx_partial',
'htmx_maybe_redirect_current_page',
) )
@ -9,3 +15,45 @@ def htmx_partial(request):
in response to an HTMX request, based on the target element. in response to an HTMX request, based on the target element.
""" """
return request.htmx and not request.htmx.boosted return request.htmx and not request.htmx.boosted
def htmx_current_url(request) -> str:
"""
Extracts the current URL from the HTMX-specific headers in the given request object.
This function checks for the `HX-Current-URL` header in the request's headers
and `HTTP_HX_CURRENT_URL` in the META data of the request. It preferentially
chooses the value present in the `HX-Current-URL` header and falls back to the
`HTTP_HX_CURRENT_URL` META data if the former is unavailable. If neither value
exists, it returns an empty string.
"""
return request.headers.get('HX-Current-URL') or request.META.get('HTTP_HX_CURRENT_URL', '') or ''
def htmx_maybe_redirect_current_page(
request, url_name: str, *, preserve_query: bool = True, status: int = 200
) -> HttpResponse | None:
"""
Redirects the current page in an HTMX request if conditions are met.
This function checks whether a request is an HTMX partial request and if the
current URL matches the provided target URL. If the conditions are met, it
returns an HTTP response signaling a redirect to the provided or updated target
URL. Otherwise, it returns None.
"""
if not htmx_partial(request):
return None
current = urlsplit(htmx_current_url(request))
target_path = reverse(url_name) # will raise NoReverseMatch if misconfigured
if current.path.rstrip('/') != target_path.rstrip('/'):
return None
redirect_to = target_path
if preserve_query and current.query:
redirect_to = f'{target_path}?{current.query}'
resp = HttpResponse(status=status)
resp['HX-Redirect'] = redirect_to
return resp