notifications.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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=('object_type', 'object_id')),
  68. )
  69. constraints = (
  70. models.UniqueConstraint(
  71. fields=('object_type', 'object_id', 'user'),
  72. name='%(app_label)s_%(class)s_unique_per_object_and_user'
  73. ),
  74. )
  75. verbose_name = _('notification')
  76. verbose_name_plural = _('notifications')
  77. def __str__(self):
  78. return self.object_repr
  79. def get_absolute_url(self):
  80. return reverse('account:notifications')
  81. def clean(self):
  82. super().clean()
  83. # Validate the assigned object type
  84. if not has_feature(self.object_type, 'notifications'):
  85. raise ValidationError(
  86. _("Objects of this type ({type}) do not support notifications.").format(type=self.object_type)
  87. )
  88. def save(self, *args, **kwargs):
  89. # Record a string representation of the associated object
  90. if self.object:
  91. self.object_repr = self.get_object_repr(self.object)
  92. super().save(*args, **kwargs)
  93. @cached_property
  94. def event(self):
  95. """
  96. Returns the registered Event which triggered this Notification.
  97. """
  98. return registry['event_types'].get(self.event_type)
  99. @classmethod
  100. def get_object_repr(cls, obj):
  101. return str(obj)[:200]
  102. class NotificationGroup(ChangeLoggedModel):
  103. """
  104. A collection of users and/or groups to be informed for certain notifications.
  105. """
  106. name = models.CharField(
  107. verbose_name=_('name'),
  108. max_length=100,
  109. unique=True
  110. )
  111. description = models.CharField(
  112. verbose_name=_('description'),
  113. max_length=200,
  114. blank=True
  115. )
  116. groups = models.ManyToManyField(
  117. to='users.Group',
  118. verbose_name=_('groups'),
  119. blank=True,
  120. related_name='notification_groups'
  121. )
  122. users = models.ManyToManyField(
  123. to='users.User',
  124. verbose_name=_('users'),
  125. blank=True,
  126. related_name='notification_groups'
  127. )
  128. event_rules = GenericRelation(
  129. to='extras.EventRule',
  130. content_type_field='action_object_type',
  131. object_id_field='action_object_id',
  132. related_query_name='+'
  133. )
  134. objects = RestrictedQuerySet.as_manager()
  135. class Meta:
  136. ordering = ('name',)
  137. verbose_name = _('notification group')
  138. verbose_name_plural = _('notification groups')
  139. def __str__(self):
  140. return self.name
  141. def get_absolute_url(self):
  142. return reverse('extras:notificationgroup', args=[self.pk])
  143. @cached_property
  144. def members(self):
  145. """
  146. Return all Users who belong to this notification group.
  147. """
  148. return self.users.union(
  149. User.objects.filter(groups__in=self.groups.all())
  150. ).order_by('username')
  151. def notify(self, object_type, object_id, **kwargs):
  152. """
  153. Bulk-create Notifications for all members of this group.
  154. """
  155. for user in self.members:
  156. Notification.objects.update_or_create(
  157. object_type=object_type,
  158. object_id=object_id,
  159. user=user,
  160. defaults=kwargs
  161. )
  162. notify.alters_data = True
  163. class Subscription(models.Model):
  164. """
  165. A User's subscription to a particular object, to be notified of changes.
  166. """
  167. created = models.DateTimeField(
  168. verbose_name=_('created'),
  169. auto_now_add=True
  170. )
  171. user = models.ForeignKey(
  172. to=settings.AUTH_USER_MODEL,
  173. on_delete=models.CASCADE,
  174. related_name='subscriptions'
  175. )
  176. object_type = models.ForeignKey(
  177. to='contenttypes.ContentType',
  178. on_delete=models.PROTECT
  179. )
  180. object_id = models.PositiveBigIntegerField()
  181. object = GenericForeignKey(
  182. ct_field='object_type',
  183. fk_field='object_id'
  184. )
  185. objects = RestrictedQuerySet.as_manager()
  186. class Meta:
  187. ordering = ('-created', 'user')
  188. indexes = (
  189. models.Index(fields=('object_type', 'object_id')),
  190. )
  191. constraints = (
  192. models.UniqueConstraint(
  193. fields=('object_type', 'object_id', 'user'),
  194. name='%(app_label)s_%(class)s_unique_per_object_and_user'
  195. ),
  196. )
  197. verbose_name = _('subscription')
  198. verbose_name_plural = _('subscriptions')
  199. def __str__(self):
  200. if self.object:
  201. return str(self.object)
  202. return super().__str__()
  203. def get_absolute_url(self):
  204. return reverse('account:subscriptions')
  205. def clean(self):
  206. super().clean()
  207. # Validate the assigned object type
  208. if not has_feature(self.object_type, 'notifications'):
  209. raise ValidationError(
  210. _("Objects of this type ({type}) do not support notifications.").format(type=self.object_type)
  211. )