change_logging.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. from django.contrib.auth.models import User
  2. from django.contrib.contenttypes.fields import GenericForeignKey
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.contrib.postgres.fields import JSONField
  5. from django.db import models
  6. from django.urls import reverse
  7. from utilities.querysets import RestrictedQuerySet
  8. from utilities.utils import serialize_object
  9. from extras.choices import *
  10. #
  11. # Change logging
  12. #
  13. class ChangeLoggedModel(models.Model):
  14. """
  15. An abstract model which adds fields to store the creation and last-updated times for an object. Both fields can be
  16. null to facilitate adding these fields to existing instances via a database migration.
  17. """
  18. created = models.DateField(
  19. auto_now_add=True,
  20. blank=True,
  21. null=True
  22. )
  23. last_updated = models.DateTimeField(
  24. auto_now=True,
  25. blank=True,
  26. null=True
  27. )
  28. class Meta:
  29. abstract = True
  30. def to_objectchange(self, action):
  31. """
  32. Return a new ObjectChange representing a change made to this object. This will typically be called automatically
  33. by extras.middleware.ChangeLoggingMiddleware.
  34. """
  35. return ObjectChange(
  36. changed_object=self,
  37. object_repr=str(self),
  38. action=action,
  39. object_data=serialize_object(self)
  40. )
  41. class ObjectChange(models.Model):
  42. """
  43. Record a change to an object and the user account associated with that change. A change record may optionally
  44. indicate an object related to the one being changed. For example, a change to an interface may also indicate the
  45. parent device. This will ensure changes made to component models appear in the parent model's changelog.
  46. """
  47. time = models.DateTimeField(
  48. auto_now_add=True,
  49. editable=False,
  50. db_index=True
  51. )
  52. user = models.ForeignKey(
  53. to=User,
  54. on_delete=models.SET_NULL,
  55. related_name='changes',
  56. blank=True,
  57. null=True
  58. )
  59. user_name = models.CharField(
  60. max_length=150,
  61. editable=False
  62. )
  63. request_id = models.UUIDField(
  64. editable=False
  65. )
  66. action = models.CharField(
  67. max_length=50,
  68. choices=ObjectChangeActionChoices
  69. )
  70. changed_object_type = models.ForeignKey(
  71. to=ContentType,
  72. on_delete=models.PROTECT,
  73. related_name='+'
  74. )
  75. changed_object_id = models.PositiveIntegerField()
  76. changed_object = GenericForeignKey(
  77. ct_field='changed_object_type',
  78. fk_field='changed_object_id'
  79. )
  80. related_object_type = models.ForeignKey(
  81. to=ContentType,
  82. on_delete=models.PROTECT,
  83. related_name='+',
  84. blank=True,
  85. null=True
  86. )
  87. related_object_id = models.PositiveIntegerField(
  88. blank=True,
  89. null=True
  90. )
  91. related_object = GenericForeignKey(
  92. ct_field='related_object_type',
  93. fk_field='related_object_id'
  94. )
  95. object_repr = models.CharField(
  96. max_length=200,
  97. editable=False
  98. )
  99. object_data = JSONField(
  100. editable=False
  101. )
  102. objects = RestrictedQuerySet.as_manager()
  103. csv_headers = [
  104. 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
  105. 'related_object_type', 'related_object_id', 'object_repr', 'object_data',
  106. ]
  107. class Meta:
  108. ordering = ['-time']
  109. def __str__(self):
  110. return '{} {} {} by {}'.format(
  111. self.changed_object_type,
  112. self.object_repr,
  113. self.get_action_display().lower(),
  114. self.user_name
  115. )
  116. def save(self, *args, **kwargs):
  117. # Record the user's name and the object's representation as static strings
  118. if not self.user_name:
  119. self.user_name = self.user.username
  120. if not self.object_repr:
  121. self.object_repr = str(self.changed_object)
  122. return super().save(*args, **kwargs)
  123. def get_absolute_url(self):
  124. return reverse('extras:objectchange', args=[self.pk])
  125. def to_csv(self):
  126. return (
  127. self.time,
  128. self.user,
  129. self.user_name,
  130. self.request_id,
  131. self.get_action_display(),
  132. self.changed_object_type,
  133. self.changed_object_id,
  134. self.related_object_type,
  135. self.related_object_id,
  136. self.object_repr,
  137. self.object_data,
  138. )