Closes #15621: User notifications (#16800)

* Initial work on #15621

* Signal receiver should ignore models which don't support notifications

* Flesh out NotificationGroup functionality

* Add NotificationGroup filters for users & groups

* Separate read & dimiss actions

* Enable one-click dismissals from notifications list

* Include total notification count in dropdown

* Drop 'kind' field from Notification model

* Register event types in the registry; add colors & icons

* Enable event rules to target notification groups

* Define dynamic choices for Notification.event_name

* Move event registration to core

* Add more job events

* Misc cleanup

* Misc cleanup

* Correct absolute URLs for notifications & subscriptions

* Optimize subscriber notifications

* Use core event types when queuing events

* Standardize queued event attribute to event_type; change content_type to object_type

* Rename Notification.event_name to event_type

* Restore NotificationGroupBulkEditView

* Add API tests

* Add view & filterset tests

* Add model documentation

* Fix tests

* Update notification bell when notifications have been cleared

* Ensure subscribe button appears only on relevant models

* Notifications/subscriptions cannot be ordered by object

* Misc cleanup

* Add event icon & type to notifications table

* Adjust icon sizing

* Mute color of read notifications

* Misc cleanup
This commit is contained in:
Jeremy Stretch
2024-07-15 14:24:11 -04:00
committed by GitHub
parent 1c2336be60
commit b0e7294bc1
59 changed files with 1913 additions and 90 deletions

View File

@@ -6,6 +6,7 @@ from django.db.models import Count, Q
from django.http import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.generic import View
@@ -356,6 +357,139 @@ class BookmarkBulkDeleteView(generic.BulkDeleteView):
return Bookmark.objects.filter(user=request.user)
#
# Notification groups
#
class NotificationGroupListView(generic.ObjectListView):
queryset = NotificationGroup.objects.all()
filterset = filtersets.NotificationGroupFilterSet
filterset_form = forms.NotificationGroupFilterForm
table = tables.NotificationGroupTable
@register_model_view(NotificationGroup)
class NotificationGroupView(generic.ObjectView):
queryset = NotificationGroup.objects.all()
@register_model_view(NotificationGroup, 'edit')
class NotificationGroupEditView(generic.ObjectEditView):
queryset = NotificationGroup.objects.all()
form = forms.NotificationGroupForm
@register_model_view(NotificationGroup, 'delete')
class NotificationGroupDeleteView(generic.ObjectDeleteView):
queryset = NotificationGroup.objects.all()
class NotificationGroupBulkImportView(generic.BulkImportView):
queryset = NotificationGroup.objects.all()
model_form = forms.NotificationGroupImportForm
class NotificationGroupBulkEditView(generic.BulkEditView):
queryset = NotificationGroup.objects.all()
filterset = filtersets.NotificationGroupFilterSet
table = tables.NotificationGroupTable
form = forms.NotificationGroupBulkEditForm
class NotificationGroupBulkDeleteView(generic.BulkDeleteView):
queryset = NotificationGroup.objects.all()
filterset = filtersets.NotificationGroupFilterSet
table = tables.NotificationGroupTable
#
# Notifications
#
class NotificationsView(LoginRequiredMixin, View):
"""
HTMX-only user-specific notifications list.
"""
def get(self, request):
return render(request, 'htmx/notifications.html', {
'notifications': request.user.notifications.unread(),
'total_count': request.user.notifications.count(),
})
@register_model_view(Notification, 'read')
class NotificationReadView(LoginRequiredMixin, View):
"""
Mark the Notification read and redirect the user to its attached object.
"""
def get(self, request, pk):
notification = get_object_or_404(request.user.notifications, pk=pk)
notification.read = timezone.now()
notification.save()
return redirect(notification.object.get_absolute_url())
@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):
return render(request, 'htmx/notifications.html', {
'notifications': request.user.notifications.unread()[:10],
})
return redirect('account:notifications')
@register_model_view(Notification, 'delete')
class NotificationDeleteView(generic.ObjectDeleteView):
def get_queryset(self, request):
return Notification.objects.filter(user=request.user)
class NotificationBulkDeleteView(generic.BulkDeleteView):
table = tables.NotificationTable
def get_queryset(self, request):
return Notification.objects.filter(user=request.user)
#
# Subscriptions
#
class SubscriptionCreateView(generic.ObjectEditView):
form = forms.SubscriptionForm
def get_queryset(self, request):
return Subscription.objects.filter(user=request.user)
def alter_object(self, obj, request, url_args, url_kwargs):
obj.user = request.user
return obj
@register_model_view(Subscription, 'delete')
class SubscriptionDeleteView(generic.ObjectDeleteView):
def get_queryset(self, request):
return Subscription.objects.filter(user=request.user)
class SubscriptionBulkDeleteView(generic.BulkDeleteView):
table = tables.SubscriptionTable
def get_queryset(self, request):
return Subscription.objects.filter(user=request.user)
#
# Webhooks
#