Skip to content

Commit

Permalink
Initial work on #15621
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Jul 3, 2024
1 parent 5ac5135 commit 865619b
Show file tree
Hide file tree
Showing 33 changed files with 959 additions and 7 deletions.
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
34 changes: 32 additions & 2 deletions netbox/account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from account.models import UserToken
from core.models import ObjectChange
from core.tables import ObjectChangeTable
from extras.models import Bookmark
from extras.tables import BookmarkTable
from extras.models import Bookmark, Notification, Subscription
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 Notification.objects.filter(user=request.user)

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 Subscription.objects.filter(user=request.user)

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


#
# User views for token management
#
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, 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', 'kind', 'event',
]
brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'user', 'kind', 'event')

@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 = Notification
fields = [
'id', 'url', 'display', 'name', 'description', 'object', '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_id', 'object_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
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
22 changes: 22 additions & 0 deletions netbox/extras/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,28 @@ 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):
metadata_class = ContentTypeMetadata
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
35 changes: 35 additions & 0 deletions netbox/extras/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,41 @@ class JournalEntryKindChoices(ChoiceSet):
]


#
# Notifications
#

class NotificationKindChoices(ChoiceSet):
key = 'Notification.kind'

KIND_INFO = 'info'
KIND_SUCCESS = 'success'
KIND_WARNING = 'warning'
KIND_DANGER = 'danger'

CHOICES = [
(KIND_INFO, _('Info'), 'cyan'),
(KIND_SUCCESS, _('Success'), 'green'),
(KIND_WARNING, _('Warning'), 'yellow'),
(KIND_DANGER, _('Danger'), 'red'),
]


# TODO: Support dynamic entries from plugins
class NotificationEventChoices(ChoiceSet):
key = 'Notification.event'

OBJECT_CREATED = 'object_created'
OBJECT_CHANGED = 'object_changed'
OBJECT_DELETED = 'object_deleted'

CHOICES = [
(OBJECT_CREATED, _('Object created')),
(OBJECT_CHANGED, _('Object changed')),
(OBJECT_DELETED, _('Object deleted')),
]


#
# Reports and Scripts
#
Expand Down
31 changes: 31 additions & 0 deletions netbox/extras/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from tenancy.models import Tenant, TenantGroup
from users.models import Group
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
from virtualization.models import Cluster, ClusterGroup, ClusterType
from .choices import *
Expand All @@ -26,6 +27,7 @@
'ImageAttachmentFilterSet',
'JournalEntryFilterSet',
'LocalConfigContextFilterSet',
'NotificationGroupFilterSet',
'ObjectTypeFilterSet',
'SavedFilterFilterSet',
'ScriptFilterSet',
Expand Down Expand Up @@ -336,6 +338,35 @@ class Meta:
fields = ('id', 'object_id')


class NotificationGroupFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
# user_id = django_filters.ModelMultipleChoiceFilter(
# queryset=get_user_model().objects.all(),
# label=_('User (ID)'),
# )
# group_id = django_filters.ModelMultipleChoiceFilter(
# queryset=Group.objects.all(),
# label=_('Group (ID)'),
# )

class Meta:
model = NotificationGroup
fields = (
'id', 'name', 'description',
)

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)


class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter(
method='search',
Expand Down
14 changes: 14 additions & 0 deletions netbox/extras/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,17 @@ class JournalEntryBulkEditForm(BulkEditForm):
required=False
)
comments = CommentField()


class NotificationGroupBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=NotificationGroup.objects.all(),
widget=forms.MultipleHiddenInput
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)

nullable_fields = ('description',)
8 changes: 8 additions & 0 deletions netbox/extras/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'EventRuleImportForm',
'ExportTemplateImportForm',
'JournalEntryImportForm',
'NotificationGroupImportForm',
'SavedFilterImportForm',
'TagImportForm',
'WebhookImportForm',
Expand Down Expand Up @@ -250,3 +251,10 @@ class Meta:
fields = (
'assigned_object_type', 'assigned_object_id', 'created_by', 'kind', 'comments', 'tags'
)


class NotificationGroupImportForm(CSVModelForm):

class Meta:
model = NotificationGroup
fields = ('name', 'description')
15 changes: 15 additions & 0 deletions netbox/extras/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from netbox.forms.base import NetBoxModelFilterSetForm
from netbox.forms.mixins import SavedFiltersMixin
from tenancy.models import Tenant, TenantGroup
from users.models import Group
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
Expand All @@ -28,6 +29,7 @@
'ImageAttachmentFilterForm',
'JournalEntryFilterForm',
'LocalConfigContextFilterForm',
'NotificationGroupFilterForm',
'SavedFilterFilterForm',
'TagFilterForm',
'WebhookFilterForm',
Expand Down Expand Up @@ -496,3 +498,16 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
required=False
)
tag = TagFilterField(model)


class NotificationGroupFilterForm(SavedFiltersMixin, FilterForm):
user_id = DynamicModelMultipleChoiceField(
queryset=get_user_model().objects.all(),
required=False,
label=_('User')
)
group_id = DynamicModelMultipleChoiceField(
queryset=Group.objects.all(),
required=False,
label=_('Group')
)
Loading

0 comments on commit 865619b

Please sign in to comment.