diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 5e2bd08c1..63491ca20 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -6,10 +6,9 @@ from taggit.managers import TaggableManager from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.fields import ASNField from dcim.models import CableTermination -from extras.models import CustomFieldModel, ObjectChange, TaggedItem +from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.utils import extras_features from utilities.querysets import RestrictedQuerySet -from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object from .choices import * from .querysets import CircuitQuerySet diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 1bd1a83ed..9b1019990 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -21,12 +21,11 @@ from dcim.choices import * from dcim.constants import * from dcim.fields import ASNField from dcim.elevations import RackElevationSVG -from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem +from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem from extras.utils import extras_features from utilities.choices import ColorChoices from utilities.fields import ColorField, NaturalOrderingField from utilities.querysets import RestrictedQuerySet -from utilities.models import ChangeLoggedModel from utilities.mptt import TreeManager from utilities.utils import serialize_object, to_meters from utilities.validators import ExclusionValidator diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py index 89dd54260..e52058157 100644 --- a/netbox/extras/models/__init__.py +++ b/netbox/extras/models/__init__.py @@ -1,11 +1,13 @@ +from .change_logging import ChangeLoggedModel, ObjectChange from .customfields import CustomField, CustomFieldChoice, CustomFieldModel, CustomFieldValue from .models import ( - ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, JobResult, ObjectChange, - Report, Script, Webhook, + ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, Graph, ImageAttachment, JobResult, Report, Script, + Webhook, ) from .tags import Tag, TaggedItem __all__ = ( + 'ChangeLoggedModel', 'ConfigContext', 'ConfigContextModel', 'CustomField', diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py new file mode 100644 index 000000000..3260c5302 --- /dev/null +++ b/netbox/extras/models/change_logging.py @@ -0,0 +1,155 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.fields import JSONField +from django.db import models +from django.urls import reverse + +from utilities.querysets import RestrictedQuerySet +from utilities.utils import serialize_object +from extras.choices import * + + +# +# Change logging +# + +class ChangeLoggedModel(models.Model): + """ + An abstract model which adds fields to store the creation and last-updated times for an object. Both fields can be + null to facilitate adding these fields to existing instances via a database migration. + """ + created = models.DateField( + auto_now_add=True, + blank=True, + null=True + ) + last_updated = models.DateTimeField( + auto_now=True, + blank=True, + null=True + ) + + class Meta: + abstract = True + + def to_objectchange(self, action): + """ + Return a new ObjectChange representing a change made to this object. This will typically be called automatically + by extras.middleware.ChangeLoggingMiddleware. + """ + return ObjectChange( + changed_object=self, + object_repr=str(self), + action=action, + object_data=serialize_object(self) + ) + + +class ObjectChange(models.Model): + """ + Record a change to an object and the user account associated with that change. A change record may optionally + indicate an object related to the one being changed. For example, a change to an interface may also indicate the + parent device. This will ensure changes made to component models appear in the parent model's changelog. + """ + time = models.DateTimeField( + auto_now_add=True, + editable=False, + db_index=True + ) + user = models.ForeignKey( + to=User, + on_delete=models.SET_NULL, + related_name='changes', + blank=True, + null=True + ) + user_name = models.CharField( + max_length=150, + editable=False + ) + request_id = models.UUIDField( + editable=False + ) + action = models.CharField( + max_length=50, + choices=ObjectChangeActionChoices + ) + changed_object_type = models.ForeignKey( + to=ContentType, + on_delete=models.PROTECT, + related_name='+' + ) + changed_object_id = models.PositiveIntegerField() + changed_object = GenericForeignKey( + ct_field='changed_object_type', + fk_field='changed_object_id' + ) + related_object_type = models.ForeignKey( + to=ContentType, + on_delete=models.PROTECT, + related_name='+', + blank=True, + null=True + ) + related_object_id = models.PositiveIntegerField( + blank=True, + null=True + ) + related_object = GenericForeignKey( + ct_field='related_object_type', + fk_field='related_object_id' + ) + object_repr = models.CharField( + max_length=200, + editable=False + ) + object_data = JSONField( + editable=False + ) + + objects = RestrictedQuerySet.as_manager() + + csv_headers = [ + 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', + 'related_object_type', 'related_object_id', 'object_repr', 'object_data', + ] + + class Meta: + ordering = ['-time'] + + def __str__(self): + return '{} {} {} by {}'.format( + self.changed_object_type, + self.object_repr, + self.get_action_display().lower(), + self.user_name + ) + + def save(self, *args, **kwargs): + + # Record the user's name and the object's representation as static strings + if not self.user_name: + self.user_name = self.user.username + if not self.object_repr: + self.object_repr = str(self.changed_object) + + return super().save(*args, **kwargs) + + def get_absolute_url(self): + return reverse('extras:objectchange', args=[self.pk]) + + def to_csv(self): + return ( + self.time, + self.user, + self.user_name, + self.request_id, + self.get_action_display(), + self.changed_object_type, + self.changed_object_id, + self.related_object_type, + self.related_object_id, + self.object_repr, + self.object_data, + ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 009fdc2d1..093589f84 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -2,7 +2,6 @@ import json import uuid from collections import OrderedDict -import django_rq from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -15,12 +14,13 @@ from django.urls import reverse from django.utils import timezone from rest_framework.utils.encoders import JSONEncoder -from utilities.querysets import RestrictedQuerySet -from utilities.utils import deepmerge, render_jinja2 from extras.choices import * from extras.constants import * +from extras.models import ChangeLoggedModel from extras.querysets import ConfigContextQuerySet from extras.utils import extras_features, FeatureQuery, image_upload +from utilities.querysets import RestrictedQuerySet +from utilities.utils import deepmerge, render_jinja2 # @@ -684,116 +684,3 @@ class JobResult(models.Model): func.delay(*args, job_id=str(job_result.job_id), job_result=job_result, **kwargs) return job_result - - -# -# Change logging -# - -class ObjectChange(models.Model): - """ - Record a change to an object and the user account associated with that change. A change record may optionally - indicate an object related to the one being changed. For example, a change to an interface may also indicate the - parent device. This will ensure changes made to component models appear in the parent model's changelog. - """ - time = models.DateTimeField( - auto_now_add=True, - editable=False, - db_index=True - ) - user = models.ForeignKey( - to=User, - on_delete=models.SET_NULL, - related_name='changes', - blank=True, - null=True - ) - user_name = models.CharField( - max_length=150, - editable=False - ) - request_id = models.UUIDField( - editable=False - ) - action = models.CharField( - max_length=50, - choices=ObjectChangeActionChoices - ) - changed_object_type = models.ForeignKey( - to=ContentType, - on_delete=models.PROTECT, - related_name='+' - ) - changed_object_id = models.PositiveIntegerField() - changed_object = GenericForeignKey( - ct_field='changed_object_type', - fk_field='changed_object_id' - ) - related_object_type = models.ForeignKey( - to=ContentType, - on_delete=models.PROTECT, - related_name='+', - blank=True, - null=True - ) - related_object_id = models.PositiveIntegerField( - blank=True, - null=True - ) - related_object = GenericForeignKey( - ct_field='related_object_type', - fk_field='related_object_id' - ) - object_repr = models.CharField( - max_length=200, - editable=False - ) - object_data = JSONField( - editable=False - ) - - objects = RestrictedQuerySet.as_manager() - - csv_headers = [ - 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', - 'related_object_type', 'related_object_id', 'object_repr', 'object_data', - ] - - class Meta: - ordering = ['-time'] - - def __str__(self): - return '{} {} {} by {}'.format( - self.changed_object_type, - self.object_repr, - self.get_action_display().lower(), - self.user_name - ) - - def save(self, *args, **kwargs): - - # Record the user's name and the object's representation as static strings - if not self.user_name: - self.user_name = self.user.username - if not self.object_repr: - self.object_repr = str(self.changed_object) - - return super().save(*args, **kwargs) - - def get_absolute_url(self): - return reverse('extras:objectchange', args=[self.pk]) - - def to_csv(self): - return ( - self.time, - self.user, - self.user_name, - self.request_id, - self.get_action_display(), - self.changed_object_type, - self.changed_object_id, - self.related_object_type, - self.related_object_id, - self.object_repr, - self.object_data, - ) diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 5ced4b962..b9d7934ce 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -2,9 +2,9 @@ from django.db import models from django.utils.text import slugify from taggit.models import TagBase, GenericTaggedItemBase +from extras.models import ChangeLoggedModel from utilities.choices import ColorChoices from utilities.fields import ColorField -from utilities.models import ChangeLoggedModel from utilities.querysets import RestrictedQuerySet diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 48884ba20..7ccb85d59 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -10,9 +10,8 @@ from django.urls import reverse from taggit.managers import TaggableManager from dcim.models import Device, Interface -from extras.models import CustomFieldModel, ObjectChange, TaggedItem +from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.utils import extras_features -from utilities.models import ChangeLoggedModel from utilities.querysets import RestrictedQuerySet from utilities.utils import serialize_object from virtualization.models import VirtualMachine, VMInterface diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index bf5858ff8..e5e53399c 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -14,9 +14,8 @@ from django.utils.encoding import force_bytes from taggit.managers import TaggableManager from dcim.models import Device -from extras.models import CustomFieldModel, TaggedItem +from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem from extras.utils import extras_features -from utilities.models import ChangeLoggedModel from utilities.querysets import RestrictedQuerySet from .exceptions import InvalidKey from .hashers import SecretValidationHasher diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 2e415b965..cc3abf19a 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -4,9 +4,8 @@ from django.urls import reverse from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager -from extras.models import CustomFieldModel, ObjectChange, TaggedItem +from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.utils import extras_features -from utilities.models import ChangeLoggedModel from utilities.mptt import TreeManager from utilities.querysets import RestrictedQuerySet from utilities.utils import serialize_object diff --git a/netbox/utilities/models.py b/netbox/utilities/models.py deleted file mode 100644 index ac3812ea8..000000000 --- a/netbox/utilities/models.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import models - -from extras.models import ObjectChange -from utilities.utils import serialize_object - - -__all__ = ( - 'ChangeLoggedModel', -) - - -class ChangeLoggedModel(models.Model): - """ - An abstract model which adds fields to store the creation and last-updated times for an object. Both fields can be - null to facilitate adding these fields to existing instances via a database migration. - """ - created = models.DateField( - auto_now_add=True, - blank=True, - null=True - ) - last_updated = models.DateTimeField( - auto_now=True, - blank=True, - null=True - ) - - class Meta: - abstract = True - - def to_objectchange(self, action): - """ - Return a new ObjectChange representing a change made to this object. This will typically be called automatically - by extras.middleware.ChangeLoggingMiddleware. - """ - return ObjectChange( - changed_object=self, - object_repr=str(self), - action=action, - object_data=serialize_object(self) - ) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 258183bff..edc6fb423 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -7,10 +7,9 @@ from taggit.managers import TaggableManager from dcim.choices import InterfaceModeChoices from dcim.models import BaseInterface, Device -from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem +from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem from extras.utils import extras_features from utilities.fields import NaturalOrderingField -from utilities.models import ChangeLoggedModel from utilities.ordering import naturalize_interface from utilities.query_functions import CollateAsChar from utilities.querysets import RestrictedQuerySet