notifications.py 6.9 KB

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