staging.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import logging
  2. from django.contrib.auth import get_user_model
  3. from django.contrib.contenttypes.fields import GenericForeignKey
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.db import models, transaction
  6. from django.utils.translation import gettext_lazy as _
  7. from extras.choices import ChangeActionChoices
  8. from netbox.models import ChangeLoggedModel
  9. from utilities.utils import deserialize_object
  10. __all__ = (
  11. 'Branch',
  12. 'StagedChange',
  13. )
  14. logger = logging.getLogger('netbox.staging')
  15. class Branch(ChangeLoggedModel):
  16. """
  17. A collection of related StagedChanges.
  18. """
  19. name = models.CharField(
  20. verbose_name=_('name'),
  21. max_length=100,
  22. unique=True
  23. )
  24. description = models.CharField(
  25. verbose_name=_('description'),
  26. max_length=200,
  27. blank=True
  28. )
  29. user = models.ForeignKey(
  30. to=get_user_model(),
  31. on_delete=models.SET_NULL,
  32. blank=True,
  33. null=True
  34. )
  35. class Meta:
  36. ordering = ('name',)
  37. verbose_name = _('branch')
  38. verbose_name_plural = _('branches')
  39. def __str__(self):
  40. return f'{self.name} ({self.pk})'
  41. def merge(self):
  42. logger.info(f'Merging changes in branch {self}')
  43. with transaction.atomic():
  44. for change in self.staged_changes.all():
  45. change.apply()
  46. self.staged_changes.all().delete()
  47. class StagedChange(ChangeLoggedModel):
  48. """
  49. The prepared creation, modification, or deletion of an object to be applied to the active database at a
  50. future point.
  51. """
  52. branch = models.ForeignKey(
  53. to=Branch,
  54. on_delete=models.CASCADE,
  55. related_name='staged_changes'
  56. )
  57. action = models.CharField(
  58. verbose_name=_('action'),
  59. max_length=20,
  60. choices=ChangeActionChoices
  61. )
  62. object_type = models.ForeignKey(
  63. to=ContentType,
  64. on_delete=models.CASCADE,
  65. related_name='+'
  66. )
  67. object_id = models.PositiveBigIntegerField(
  68. blank=True,
  69. null=True
  70. )
  71. object = GenericForeignKey(
  72. ct_field='object_type',
  73. fk_field='object_id'
  74. )
  75. data = models.JSONField(
  76. verbose_name=_('data'),
  77. blank=True,
  78. null=True
  79. )
  80. class Meta:
  81. ordering = ('pk',)
  82. verbose_name = _('staged change')
  83. verbose_name_plural = _('staged changes')
  84. def __str__(self):
  85. action = self.get_action_display()
  86. app_label, model_name = self.object_type.natural_key()
  87. return f"{action} {app_label}.{model_name} ({self.object_id})"
  88. @property
  89. def model(self):
  90. return self.object_type.model_class()
  91. def apply(self):
  92. """
  93. Apply the staged create/update/delete action to the database.
  94. """
  95. if self.action == ChangeActionChoices.ACTION_CREATE:
  96. instance = deserialize_object(self.model, self.data, pk=self.object_id)
  97. logger.info(f'Creating {self.model._meta.verbose_name} {instance}')
  98. instance.save()
  99. if self.action == ChangeActionChoices.ACTION_UPDATE:
  100. instance = deserialize_object(self.model, self.data, pk=self.object_id)
  101. logger.info(f'Updating {self.model._meta.verbose_name} {instance}')
  102. instance.save()
  103. if self.action == ChangeActionChoices.ACTION_DELETE:
  104. instance = self.model.objects.get(pk=self.object_id)
  105. logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
  106. instance.delete()
  107. apply.alters_data = True
  108. def get_action_color(self):
  109. return ChangeActionChoices.colors.get(self.action)