Просмотр исходного кода

Merge pull request #20612 from pheus/20301-add-clear-all-option-to-user-notifications-dropdown

Closes #20301: Add "Dismiss all" action to notifications dropdown
bctiemann 3 месяцев назад
Родитель
Сommit
3d4841f17f
3 измененных файлов с 94 добавлено и 3 удалено
  1. 35 3
      netbox/extras/views.py
  2. 11 0
      netbox/templates/htmx/notifications.html
  3. 48 0
      netbox/utilities/htmx.py

+ 35 - 3
netbox/extras/views.py

@@ -4,7 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.contenttypes.models import ContentType
 from django.core.paginator import EmptyPage
 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.urls import reverse
 from django.utils import timezone
@@ -25,7 +25,7 @@ from netbox.object_actions import *
 from netbox.views import generic
 from netbox.views.generic.mixins import TableMixin
 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.query import count_related
 from utilities.querydict import normalize_querydict
@@ -525,8 +525,9 @@ class NotificationsView(LoginRequiredMixin, View):
     """
     def get(self, request):
         return render(request, 'htmx/notifications.html', {
-            'notifications': request.user.notifications.unread(),
+            'notifications': request.user.notifications.unread()[:10],
             'total_count': request.user.notifications.count(),
+            'unread_count': request.user.notifications.unread().count(),
         })
 
 
@@ -535,6 +536,7 @@ class NotificationReadView(LoginRequiredMixin, View):
     """
     Mark the Notification read and redirect the user to its attached object.
     """
+
     def get(self, request, pk):
         # Mark the Notification as read
         notification = get_object_or_404(request.user.notifications, pk=pk)
@@ -548,18 +550,48 @@ class NotificationReadView(LoginRequiredMixin, View):
         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')
 class NotificationDismissView(LoginRequiredMixin, View):
     """
     A convenience view which allows deleting notifications with one click.
     """
+
     def get(self, request, pk):
         notification = get_object_or_404(request.user.notifications, pk=pk)
         notification.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')

+ 11 - 0
netbox/templates/htmx/notifications.html

@@ -1,4 +1,15 @@
 {% 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">
   {% for notification in notifications %}
     <div class="list-group-item p-2">

+ 48 - 0
netbox/utilities/htmx.py

@@ -1,5 +1,11 @@
+from django.http import HttpResponse
+from django.urls import reverse
+from urllib.parse import urlsplit
+
 __all__ = (
+    'htmx_current_url',
     '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.
     """
     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