change_logging.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. from django.conf import settings
  2. from django.contrib.contenttypes.fields import GenericForeignKey
  3. from django.core.exceptions import ValidationError
  4. from django.db import models
  5. from django.urls import reverse
  6. from django.utils.translation import gettext_lazy as _
  7. from core.models import ContentType
  8. from extras.choices import *
  9. from ..querysets import ObjectChangeQuerySet
  10. __all__ = (
  11. 'ObjectChange',
  12. )
  13. class ObjectChange(models.Model):
  14. """
  15. Record a change to an object and the user account associated with that change. A change record may optionally
  16. indicate an object related to the one being changed. For example, a change to an interface may also indicate the
  17. parent device. This will ensure changes made to component models appear in the parent model's changelog.
  18. """
  19. time = models.DateTimeField(
  20. verbose_name=_('time'),
  21. auto_now_add=True,
  22. editable=False,
  23. db_index=True
  24. )
  25. user = models.ForeignKey(
  26. to=settings.AUTH_USER_MODEL,
  27. on_delete=models.SET_NULL,
  28. related_name='changes',
  29. blank=True,
  30. null=True
  31. )
  32. user_name = models.CharField(
  33. verbose_name=_('user name'),
  34. max_length=150,
  35. editable=False
  36. )
  37. request_id = models.UUIDField(
  38. verbose_name=_('request ID'),
  39. editable=False,
  40. db_index=True
  41. )
  42. action = models.CharField(
  43. verbose_name=_('action'),
  44. max_length=50,
  45. choices=ObjectChangeActionChoices
  46. )
  47. changed_object_type = models.ForeignKey(
  48. to='contenttypes.ContentType',
  49. on_delete=models.PROTECT,
  50. related_name='+'
  51. )
  52. changed_object_id = models.PositiveBigIntegerField()
  53. changed_object = GenericForeignKey(
  54. ct_field='changed_object_type',
  55. fk_field='changed_object_id'
  56. )
  57. related_object_type = models.ForeignKey(
  58. to='contenttypes.ContentType',
  59. on_delete=models.PROTECT,
  60. related_name='+',
  61. blank=True,
  62. null=True
  63. )
  64. related_object_id = models.PositiveBigIntegerField(
  65. blank=True,
  66. null=True
  67. )
  68. related_object = GenericForeignKey(
  69. ct_field='related_object_type',
  70. fk_field='related_object_id'
  71. )
  72. object_repr = models.CharField(
  73. max_length=200,
  74. editable=False
  75. )
  76. prechange_data = models.JSONField(
  77. verbose_name=_('pre-change data'),
  78. editable=False,
  79. blank=True,
  80. null=True
  81. )
  82. postchange_data = models.JSONField(
  83. verbose_name=_('post-change data'),
  84. editable=False,
  85. blank=True,
  86. null=True
  87. )
  88. objects = ObjectChangeQuerySet.as_manager()
  89. class Meta:
  90. ordering = ['-time']
  91. indexes = (
  92. models.Index(fields=('changed_object_type', 'changed_object_id')),
  93. models.Index(fields=('related_object_type', 'related_object_id')),
  94. )
  95. verbose_name = _('object change')
  96. verbose_name_plural = _('object changes')
  97. def __str__(self):
  98. return '{} {} {} by {}'.format(
  99. self.changed_object_type,
  100. self.object_repr,
  101. self.get_action_display().lower(),
  102. self.user_name
  103. )
  104. def clean(self):
  105. super().clean()
  106. # Validate the assigned object type
  107. if self.changed_object_type not in ContentType.objects.with_feature('change_logging'):
  108. raise ValidationError(
  109. _("Change logging is not supported for this object type ({type}).").format(
  110. type=self.changed_object_type
  111. )
  112. )
  113. def save(self, *args, **kwargs):
  114. # Record the user's name and the object's representation as static strings
  115. if not self.user_name:
  116. self.user_name = self.user.username
  117. if not self.object_repr:
  118. self.object_repr = str(self.changed_object)
  119. return super().save(*args, **kwargs)
  120. def get_absolute_url(self):
  121. return reverse('extras:objectchange', args=[self.pk])
  122. def get_action_color(self):
  123. return ObjectChangeActionChoices.colors.get(self.action)