Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions netbox/extras/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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
Comment on lines -7 to 8
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This is an unrelated fix. Http404 is used, but not imported.

from django.urls import reverse
from django.utils import timezone
Expand All @@ -25,7 +25,7 @@
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
Expand Down Expand Up @@ -518,8 +518,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(),
Comment on lines -521 to 522
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During manual testing, I've noticed that the dropdown menu does not actually limit the notification.

'unread_count': request.user.notifications.unread().count(),
})


Expand All @@ -528,6 +529,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)
Expand All @@ -541,18 +543,48 @@ def get(self, request, pk):
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')
Expand Down
11 changes: 11 additions & 0 deletions netbox/templates/htmx/notifications.html
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
48 changes: 48 additions & 0 deletions netbox/utilities/htmx.py
Original file line number Diff line number Diff line change
@@ -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',
)


Expand All @@ -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