mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-09 01:49:35 -06:00
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:
parent
cb3308a166
commit
8eaff9dce7
@ -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')
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user