Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
865619b
Initial work on #15621
jeremystretch Jul 3, 2024
b0e37bc
Signal receiver should ignore models which don't support notifications
jeremystretch Jul 3, 2024
896c174
Flesh out NotificationGroup functionality
jeremystretch Jul 3, 2024
a21d4fe
Add NotificationGroup filters for users & groups
jeremystretch Jul 4, 2024
9c87571
Separate read & dimiss actions
jeremystretch Jul 4, 2024
29675d6
Enable one-click dismissals from notifications list
jeremystretch Jul 4, 2024
1c81183
Include total notification count in dropdown
jeremystretch Jul 4, 2024
8065d7c
Drop 'kind' field from Notification model
jeremystretch Jul 4, 2024
8c0693f
Register event types in the registry; add colors & icons
jeremystretch Jul 4, 2024
71860ba
Enable event rules to target notification groups
jeremystretch Jul 4, 2024
c22b705
Define dynamic choices for Notification.event_name
jeremystretch Jul 4, 2024
f129738
Move event registration to core
jeremystretch Jul 4, 2024
ba11e97
Add more job events
jeremystretch Jul 4, 2024
6bec89d
Misc cleanup
jeremystretch Jul 4, 2024
cbffc24
Misc cleanup
jeremystretch Jul 4, 2024
9f6ccec
Correct absolute URLs for notifications & subscriptions
jeremystretch Jul 4, 2024
2bc7b20
Optimize subscriber notifications
jeremystretch Jul 4, 2024
1dd5d0a
Use core event types when queuing events
jeremystretch Jul 8, 2024
744d3d8
Standardize queued event attribute to event_type; change content_type…
jeremystretch Jul 8, 2024
fa722fe
Rename Notification.event_name to event_type
jeremystretch Jul 8, 2024
0d553ca
Restore NotificationGroupBulkEditView
jeremystretch Jul 8, 2024
fdbcd36
Add API tests
jeremystretch Jul 8, 2024
623c012
Add view & filterset tests
jeremystretch Jul 10, 2024
f82de2b
Merge branch 'feature' into 15621-notifications
jeremystretch Jul 11, 2024
1e57915
Add model documentation
jeremystretch Jul 11, 2024
5131cc9
Fix tests
jeremystretch Jul 11, 2024
4f0cc95
Update notification bell when notifications have been cleared
jeremystretch Jul 11, 2024
aa8a891
Ensure subscribe button appears only on relevant models
jeremystretch Jul 12, 2024
854b745
Notifications/subscriptions cannot be ordered by object
jeremystretch Jul 12, 2024
b94cb88
Misc cleanup
jeremystretch Jul 12, 2024
2f2e273
Add event icon & type to notifications table
jeremystretch Jul 12, 2024
bfd0fa4
Adjust icon sizing
jeremystretch Jul 12, 2024
9fc0c83
Mute color of read notifications
jeremystretch Jul 12, 2024
7856da4
Misc cleanup
jeremystretch Jul 15, 2024
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
17 changes: 17 additions & 0 deletions docs/models/extras/notification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Notification

A notification alerts a user that a specific action has taken place in NetBox, such as an object being modified or a background job completing. A notification may be generated via a user's [subscription](./subscription.md) to a particular object, or by an event rule targeting a [notification group](./notificationgroup.md) of which the user is a member.

## Fields

### User

The recipient of the notification.

### Object

The object to which the notification relates.

### Event Type

The type of event indicated by the notification.
17 changes: 17 additions & 0 deletions docs/models/extras/notificationgroup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Notification Group

A set of NetBox users and/or groups of users identified as recipients for certain [notifications](./notification.md).

## Fields

### Name

The name of the notification group.

### Users

One or more users directly designated as members of the notification group.

### Groups

All users of any selected groups are considered as members of the notification group.
15 changes: 15 additions & 0 deletions docs/models/extras/subscription.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Subscription

A record indicating that a user is to be notified of any changes to a particular NetBox object. A notification maps exactly one user to exactly one object.

When an object to which a user is subscribed changes, a [notification](./notification.md) is generated for the user.

## Fields

### User

The subscribed user.

### Object

The object to which the user is subscribed.
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,11 @@ nav:
- ExportTemplate: 'models/extras/exporttemplate.md'
- ImageAttachment: 'models/extras/imageattachment.md'
- JournalEntry: 'models/extras/journalentry.md'
- Notification: 'models/extras/notification.md'
- NotificationGroup: 'models/extras/notificationgroup.md'
- SavedFilter: 'models/extras/savedfilter.md'
- StagedChange: 'models/extras/stagedchange.md'
- Subscription: 'models/extras/subscription.md'
- Tag: 'models/extras/tag.md'
- Webhook: 'models/extras/webhook.md'
- IPAM:
Expand Down
2 changes: 2 additions & 0 deletions netbox/account/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# Account views
path('profile/', views.ProfileView.as_view(), name='profile'),
path('bookmarks/', views.BookmarkListView.as_view(), name='bookmarks'),
path('notifications/', views.NotificationListView.as_view(), name='notifications'),
path('subscriptions/', views.SubscriptionListView.as_view(), name='subscriptions'),
path('preferences/', views.UserConfigView.as_view(), name='preferences'),
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
Expand Down
32 changes: 31 additions & 1 deletion netbox/account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from core.models import ObjectChange
from core.tables import ObjectChangeTable
from extras.models import Bookmark
from extras.tables import BookmarkTable
from extras.tables import BookmarkTable, NotificationTable, SubscriptionTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from netbox.views import generic
Expand Down Expand Up @@ -267,6 +267,36 @@ def get_extra_context(self, request):
}


#
# Notifications & subscriptions
#

class NotificationListView(LoginRequiredMixin, generic.ObjectListView):
table = NotificationTable
template_name = 'account/notifications.html'

def get_queryset(self, request):
return request.user.notifications.all()

def get_extra_context(self, request):
return {
'active_tab': 'notifications',
}


class SubscriptionListView(LoginRequiredMixin, generic.ObjectListView):
table = SubscriptionTable
template_name = 'account/subscriptions.html'

def get_queryset(self, request):
return request.user.subscriptions.all()

def get_extra_context(self, request):
return {
'active_tab': 'subscriptions',
}


#
# User views for token management
#
Expand Down
2 changes: 1 addition & 1 deletion netbox/core/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class CoreConfig(AppConfig):
def ready(self):
from core.api import schema # noqa
from netbox.models.features import register_models
from . import data_backends, search
from . import data_backends, events, search

# Register models
register_models(*self.get_models())
33 changes: 33 additions & 0 deletions netbox/core/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.utils.translation import gettext as _

from netbox.events import *

__all__ = (
'JOB_COMPLETED',
'JOB_ERRORED',
'JOB_FAILED',
'JOB_STARTED',
'OBJECT_CREATED',
'OBJECT_DELETED',
'OBJECT_UPDATED',
)

# Object events
OBJECT_CREATED = 'object_created'
OBJECT_UPDATED = 'object_updated'
OBJECT_DELETED = 'object_deleted'

# Job events
JOB_STARTED = 'job_started'
JOB_COMPLETED = 'job_completed'
JOB_FAILED = 'job_failed'
JOB_ERRORED = 'job_errored'

# Register core events
Event(name=OBJECT_CREATED, text=_('Object created')).register()
Event(name=OBJECT_UPDATED, text=_('Object updated')).register()
Event(name=OBJECT_DELETED, text=_('Object deleted')).register()
Event(name=JOB_STARTED, text=_('Job started')).register()
Event(name=JOB_COMPLETED, text=_('Job completed'), type=EVENT_TYPE_SUCCESS).register()
Event(name=JOB_FAILED, text=_('Job failed'), type=EVENT_TYPE_WARNING).register()
Event(name=JOB_ERRORED, text=_('Job errored'), type=EVENT_TYPE_DANGER).register()
1 change: 0 additions & 1 deletion netbox/core/models/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from core.choices import JobStatusChoices
from core.models import ObjectType
from core.signals import job_end, job_start
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
from netbox.config import get_config
from netbox.constants import RQ_QUEUE_DEFAULT
from utilities.querysets import RestrictedQuerySet
Expand Down
1 change: 1 addition & 0 deletions netbox/extras/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .serializers_.events import *
from .serializers_.exporttemplates import *
from .serializers_.journaling import *
from .serializers_.notifications import *
from .serializers_.configcontexts import *
from .serializers_.configtemplates import *
from .serializers_.savedfilters import *
Expand Down
82 changes: 82 additions & 0 deletions netbox/extras/api/serializers_/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from core.models import ObjectType
from extras.models import Notification, NotificationGroup, Subscription
from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import ValidatedModelSerializer
from users.api.serializers_.users import GroupSerializer, UserSerializer
from users.models import Group, User
from utilities.api import get_serializer_for_model

__all__ = (
'NotificationSerializer',
'NotificationGroupSerializer',
'SubscriptionSerializer',
)


class NotificationSerializer(ValidatedModelSerializer):
object_type = ContentTypeField(
queryset=ObjectType.objects.with_feature('notifications'),
)
object = serializers.SerializerMethodField(read_only=True)
user = UserSerializer(nested=True)

class Meta:
model = Notification
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', 'read', 'event_type',
]
brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'user', 'read', 'event_type')

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, instance):
serializer = get_serializer_for_model(instance.object)
context = {'request': self.context['request']}
return serializer(instance.object, nested=True, context=context).data


class NotificationGroupSerializer(ValidatedModelSerializer):
groups = SerializedPKRelatedField(
queryset=Group.objects.all(),
serializer=GroupSerializer,
nested=True,
required=False,
many=True
)
users = SerializedPKRelatedField(
queryset=User.objects.all(),
serializer=UserSerializer,
nested=True,
required=False,
many=True
)

class Meta:
model = NotificationGroup
fields = [
'id', 'url', 'display', 'display_url', 'name', 'description', 'groups', 'users',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')


class SubscriptionSerializer(ValidatedModelSerializer):
object_type = ContentTypeField(
queryset=ObjectType.objects.with_feature('notifications'),
)
object = serializers.SerializerMethodField(read_only=True)
user = UserSerializer(nested=True)

class Meta:
model = Subscription
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
]
brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'user')

@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, instance):
serializer = get_serializer_for_model(instance.object)
context = {'request': self.context['request']}
return serializer(instance.object, nested=True, context=context).data
3 changes: 3 additions & 0 deletions netbox/extras/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
router.register('export-templates', views.ExportTemplateViewSet)
router.register('saved-filters', views.SavedFilterViewSet)
router.register('bookmarks', views.BookmarkViewSet)
router.register('notifications', views.NotificationViewSet)
router.register('notification-groups', views.NotificationGroupViewSet)
router.register('subscriptions', views.SubscriptionViewSet)
router.register('tags', views.TagViewSet)
router.register('image-attachments', views.ImageAttachmentViewSet)
router.register('journal-entries', views.JournalEntryViewSet)
Expand Down
21 changes: 21 additions & 0 deletions netbox/extras/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,27 @@ class BookmarkViewSet(NetBoxModelViewSet):
filterset_class = filtersets.BookmarkFilterSet


#
# Notifications & subscriptions
#

class NotificationViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Notification.objects.all()
serializer_class = serializers.NotificationSerializer


class NotificationGroupViewSet(NetBoxModelViewSet):
queryset = NotificationGroup.objects.all()
serializer_class = serializers.NotificationGroupSerializer


class SubscriptionViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Subscription.objects.all()
serializer_class = serializers.SubscriptionSerializer


#
# Tags
#
Expand Down
2 changes: 2 additions & 0 deletions netbox/extras/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,10 @@ class EventRuleActionChoices(ChoiceSet):

WEBHOOK = 'webhook'
SCRIPT = 'script'
NOTIFICATION = 'notification'

CHOICES = (
(WEBHOOK, _('Webhook')),
(SCRIPT, _('Script')),
(NOTIFICATION, _('Notification')),
)
21 changes: 9 additions & 12 deletions netbox/extras/constants.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
from core.events import *
from extras.choices import LogLevelChoices

# Events
EVENT_CREATE = 'create'
EVENT_UPDATE = 'update'
EVENT_DELETE = 'delete'
EVENT_JOB_START = 'job_start'
EVENT_JOB_END = 'job_end'

# Custom fields
CUSTOMFIELD_EMPTY_VALUES = (None, '', [])

# Webhooks
HTTP_CONTENT_TYPE_JSON = 'application/json'

WEBHOOK_EVENT_TYPES = {
EVENT_CREATE: 'created',
EVENT_UPDATE: 'updated',
EVENT_DELETE: 'deleted',
EVENT_JOB_START: 'job_started',
EVENT_JOB_END: 'job_ended',
# Map registered event types to public webhook "event" equivalents
OBJECT_CREATED: 'created',
OBJECT_UPDATED: 'updated',
OBJECT_DELETED: 'deleted',
JOB_STARTED: 'job_started',
JOB_COMPLETED: 'job_ended',
JOB_FAILED: 'job_ended',
JOB_ERRORED: 'job_ended',
}

# Dashboard
Expand Down
Loading