Coverage for extras/models/notifications.py: 70%
96 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from functools import cached_property
3from django.conf import settings
4from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
5from django.core.exceptions import ValidationError
6from django.db import models
7from django.urls import reverse
8from django.utils.translation import gettext_lazy as _
10from extras.querysets import NotificationQuerySet
11from netbox.models import ChangeLoggedModel
12from netbox.models.features import has_feature
13from netbox.registry import registry
14from users.models import User
15from utilities.choices import Choice
16from utilities.querysets import RestrictedQuerySet
18__all__ = (
19 'Notification',
20 'NotificationGroup',
21 'Subscription',
22)
25def get_event_type_choices():
26 """
27 Compile a list of choices from all registered event types
28 """
29 return [
30 Choice(name, event.text)
31 for name, event in registry['event_types'].items()
32 ]
35class Notification(models.Model):
36 """
37 A notification message for a User relating to a specific object in NetBox.
38 """
39 created = models.DateTimeField(
40 verbose_name=_('created'),
41 auto_now_add=True
42 )
43 read = models.DateTimeField(
44 verbose_name=_('read'),
45 null=True,
46 blank=True
47 )
48 user = models.ForeignKey(
49 to=settings.AUTH_USER_MODEL,
50 on_delete=models.CASCADE,
51 related_name='notifications'
52 )
53 object_type = models.ForeignKey(
54 to='contenttypes.ContentType',
55 on_delete=models.PROTECT
56 )
57 object_id = models.PositiveBigIntegerField()
58 object = GenericForeignKey(
59 ct_field='object_type',
60 fk_field='object_id'
61 )
62 object_repr = models.CharField(
63 max_length=200,
64 editable=False
65 )
66 event_type = models.CharField(
67 verbose_name=_('event'),
68 max_length=50,
69 choices=get_event_type_choices
70 )
72 objects = NotificationQuerySet.as_manager()
74 class Meta:
75 ordering = ('-created', 'pk')
76 indexes = (
77 models.Index(fields=('-created', 'id')), # Default ordering
78 models.Index(fields=('object_type', 'object_id')),
79 )
80 constraints = (
81 models.UniqueConstraint(
82 fields=('object_type', 'object_id', 'user'),
83 name='%(app_label)s_%(class)s_unique_per_object_and_user'
84 ),
85 )
86 verbose_name = _('notification')
87 verbose_name_plural = _('notifications')
89 def __str__(self):
90 return self.object_repr
92 def get_absolute_url(self):
93 return reverse('account:notifications')
95 def clean(self):
96 super().clean()
98 # Validate the assigned object type
99 if not has_feature(self.object_type, 'notifications'):
100 raise ValidationError(
101 _("Objects of this type ({type}) do not support notifications.").format(type=self.object_type)
102 )
104 def save(self, *args, **kwargs):
105 # Record a string representation of the associated object
106 if self.object:
107 self.object_repr = self.get_object_repr(self.object)
109 super().save(*args, **kwargs)
111 @cached_property
112 def event(self):
113 """
114 Returns the registered Event which triggered this Notification.
115 """
116 return registry['event_types'].get(self.event_type)
118 @classmethod
119 def get_object_repr(cls, obj):
120 return str(obj)[:200]
123class NotificationGroup(ChangeLoggedModel):
124 """
125 A collection of users and/or groups to be informed for certain notifications.
126 """
127 name = models.CharField(
128 verbose_name=_('name'),
129 max_length=100,
130 unique=True
131 )
132 description = models.CharField(
133 verbose_name=_('description'),
134 max_length=200,
135 blank=True
136 )
137 groups = models.ManyToManyField(
138 to='users.Group',
139 verbose_name=_('groups'),
140 blank=True,
141 related_name='notification_groups'
142 )
143 users = models.ManyToManyField(
144 to='users.User',
145 verbose_name=_('users'),
146 blank=True,
147 related_name='notification_groups'
148 )
149 event_rules = GenericRelation(
150 to='extras.EventRule',
151 content_type_field='action_object_type',
152 object_id_field='action_object_id',
153 related_query_name='+'
154 )
156 objects = RestrictedQuerySet.as_manager()
158 class Meta:
159 ordering = ('name',)
160 verbose_name = _('notification group')
161 verbose_name_plural = _('notification groups')
163 def __str__(self):
164 return self.name
166 def get_absolute_url(self):
167 return reverse('extras:notificationgroup', args=[self.pk])
169 @cached_property
170 def members(self):
171 """
172 Return all Users who belong to this notification group.
173 """
174 return self.users.union(
175 User.objects.filter(groups__in=self.groups.all())
176 ).order_by('username')
178 def notify(self, object_type, object_id, **kwargs):
179 """
180 Bulk-create Notifications for all members of this group.
181 """
182 for user in self.members:
183 Notification.objects.update_or_create(
184 object_type=object_type,
185 object_id=object_id,
186 user=user,
187 defaults=kwargs
188 )
189 notify.alters_data = True
192class Subscription(models.Model):
193 """
194 A User's subscription to a particular object, to be notified of changes.
195 """
196 created = models.DateTimeField(
197 verbose_name=_('created'),
198 auto_now_add=True
199 )
200 user = models.ForeignKey(
201 to=settings.AUTH_USER_MODEL,
202 on_delete=models.CASCADE,
203 related_name='subscriptions'
204 )
205 object_type = models.ForeignKey(
206 to='contenttypes.ContentType',
207 on_delete=models.PROTECT
208 )
209 object_id = models.PositiveBigIntegerField()
210 object = GenericForeignKey(
211 ct_field='object_type',
212 fk_field='object_id'
213 )
215 objects = RestrictedQuerySet.as_manager()
217 class Meta:
218 ordering = ('-created', 'user')
219 indexes = (
220 models.Index(fields=('-created', 'user')), # Default ordering
221 models.Index(fields=('object_type', 'object_id')),
222 )
223 constraints = (
224 models.UniqueConstraint(
225 fields=('object_type', 'object_id', 'user'),
226 name='%(app_label)s_%(class)s_unique_per_object_and_user'
227 ),
228 )
229 verbose_name = _('subscription')
230 verbose_name_plural = _('subscriptions')
232 def __str__(self):
233 if self.object:
234 return str(self.object)
235 return super().__str__()
237 def get_absolute_url(self):
238 return reverse('account:subscriptions')
240 def clean(self):
241 super().clean()
243 # Validate the assigned object type
244 if not has_feature(self.object_type, 'notifications'):
245 raise ValidationError(
246 _("Objects of this type ({type}) do not support notifications.").format(type=self.object_type)
247 )