Skip to content

Commit 8eaff9d

Browse files
committed
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
1 parent cb3308a commit 8eaff9d

File tree

3 files changed

+94
-3
lines changed

3 files changed

+94
-3
lines changed

netbox/extras/views.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.contrib.contenttypes.models import ContentType
55
from django.core.paginator import EmptyPage
66
from django.db.models import Count, Q
7-
from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
7+
from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse, Http404
88
from django.shortcuts import get_object_or_404, redirect, render
99
from django.urls import reverse
1010
from django.utils import timezone
@@ -25,7 +25,7 @@
2525
from netbox.views import generic
2626
from netbox.views.generic.mixins import TableMixin
2727
from utilities.forms import ConfirmationForm, get_field_value
28-
from utilities.htmx import htmx_partial
28+
from utilities.htmx import htmx_partial, htmx_maybe_redirect_current_page
2929
from utilities.paginator import EnhancedPaginator, get_paginate_count
3030
from utilities.query import count_related
3131
from utilities.querydict import normalize_querydict
@@ -518,8 +518,9 @@ class NotificationsView(LoginRequiredMixin, View):
518518
"""
519519
def get(self, request):
520520
return render(request, 'htmx/notifications.html', {
521-
'notifications': request.user.notifications.unread(),
521+
'notifications': request.user.notifications.unread()[:10],
522522
'total_count': request.user.notifications.count(),
523+
'unread_count': request.user.notifications.unread().count(),
523524
})
524525

525526

@@ -528,6 +529,7 @@ class NotificationReadView(LoginRequiredMixin, View):
528529
"""
529530
Mark the Notification read and redirect the user to its attached object.
530531
"""
532+
531533
def get(self, request, pk):
532534
# Mark the Notification as read
533535
notification = get_object_or_404(request.user.notifications, pk=pk)
@@ -541,18 +543,48 @@ def get(self, request, pk):
541543
return redirect('account:notifications')
542544

543545

546+
@register_model_view(Notification, name='dismiss_all', path='dismiss-all', detail=False)
547+
class NotificationDismissAllView(LoginRequiredMixin, View):
548+
"""
549+
Convenience view to clear all *unread* notifications for the current user.
550+
"""
551+
552+
def get(self, request):
553+
request.user.notifications.unread().delete()
554+
if htmx_partial(request):
555+
# If a user is currently on the notification page, redirect there (full repaint)
556+
redirect_resp = htmx_maybe_redirect_current_page(request, 'account:notifications', preserve_query=True)
557+
if redirect_resp:
558+
return redirect_resp
559+
560+
return render(request, 'htmx/notifications.html', {
561+
'notifications': request.user.notifications.unread()[:10],
562+
'total_count': request.user.notifications.count(),
563+
'unread_count': request.user.notifications.unread().count(),
564+
})
565+
return redirect('account:notifications')
566+
567+
544568
@register_model_view(Notification, 'dismiss')
545569
class NotificationDismissView(LoginRequiredMixin, View):
546570
"""
547571
A convenience view which allows deleting notifications with one click.
548572
"""
573+
549574
def get(self, request, pk):
550575
notification = get_object_or_404(request.user.notifications, pk=pk)
551576
notification.delete()
552577

553578
if htmx_partial(request):
579+
# If a user is currently on the notification page, redirect there (full repaint)
580+
redirect_resp = htmx_maybe_redirect_current_page(request, 'account:notifications', preserve_query=True)
581+
if redirect_resp:
582+
return redirect_resp
583+
554584
return render(request, 'htmx/notifications.html', {
555585
'notifications': request.user.notifications.unread()[:10],
586+
'total_count': request.user.notifications.count(),
587+
'unread_count': request.user.notifications.unread().count(),
556588
})
557589

558590
return redirect('account:notifications')

netbox/templates/htmx/notifications.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
{% load i18n %}
2+
<div class="card-header px-2 py-1">
3+
<h3 class="card-title flex-fill">Notifications</h3>
4+
{% if notifications %}
5+
<a href="#" hx-get="{% url 'extras:notification_dismiss_all' %}" hx-target="closest .notifications"
6+
hx-confirm="{% blocktrans trimmed count count=unread_count %}Dismiss {{ count }} unread notification?{% plural %}Dismiss {{ count }} unread notifications?{% endblocktrans %}"
7+
class="btn btn-2 text-danger" title="{% trans 'Dismiss all unread notifications' %}">
8+
<i class="icon mdi mdi-delete-sweep-outline"></i>
9+
{% trans "Dismiss all" %}
10+
</a>
11+
{% endif %}
12+
</div>
213
<div class="list-group list-group-flush list-group-hoverable" style="min-width: 300px">
314
{% for notification in notifications %}
415
<div class="list-group-item p-2">

netbox/utilities/htmx.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
from django.http import HttpResponse
2+
from django.urls import reverse
3+
from urllib.parse import urlsplit
4+
15
__all__ = (
6+
'htmx_current_url',
27
'htmx_partial',
8+
'htmx_maybe_redirect_current_page',
39
)
410

511

@@ -9,3 +15,45 @@ def htmx_partial(request):
915
in response to an HTMX request, based on the target element.
1016
"""
1117
return request.htmx and not request.htmx.boosted
18+
19+
20+
def htmx_current_url(request) -> str:
21+
"""
22+
Extracts the current URL from the HTMX-specific headers in the given request object.
23+
24+
This function checks for the `HX-Current-URL` header in the request's headers
25+
and `HTTP_HX_CURRENT_URL` in the META data of the request. It preferentially
26+
chooses the value present in the `HX-Current-URL` header and falls back to the
27+
`HTTP_HX_CURRENT_URL` META data if the former is unavailable. If neither value
28+
exists, it returns an empty string.
29+
"""
30+
return request.headers.get('HX-Current-URL') or request.META.get('HTTP_HX_CURRENT_URL', '') or ''
31+
32+
33+
def htmx_maybe_redirect_current_page(
34+
request, url_name: str, *, preserve_query: bool = True, status: int = 200
35+
) -> HttpResponse | None:
36+
"""
37+
Redirects the current page in an HTMX request if conditions are met.
38+
39+
This function checks whether a request is an HTMX partial request and if the
40+
current URL matches the provided target URL. If the conditions are met, it
41+
returns an HTTP response signaling a redirect to the provided or updated target
42+
URL. Otherwise, it returns None.
43+
"""
44+
if not htmx_partial(request):
45+
return None
46+
47+
current = urlsplit(htmx_current_url(request))
48+
target_path = reverse(url_name) # will raise NoReverseMatch if misconfigured
49+
50+
if current.path.rstrip('/') != target_path.rstrip('/'):
51+
return None
52+
53+
redirect_to = target_path
54+
if preserve_query and current.query:
55+
redirect_to = f'{target_path}?{current.query}'
56+
57+
resp = HttpResponse(status=status)
58+
resp['HX-Redirect'] = redirect_to
59+
return resp

0 commit comments

Comments
 (0)