mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Implemented new object change logging to replace UserActions
This commit is contained in:
@ -5,9 +5,9 @@ from django.contrib import admin
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from utilities.forms import LaxURLField
|
||||
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from .models import (
|
||||
CustomField, CustomFieldChoice, Graph, ExportTemplate, TopologyMap, UserAction,
|
||||
Webhook
|
||||
CustomField, CustomFieldChoice, Graph, ExportTemplate, ObjectChange, TopologyMap, UserAction, Webhook,
|
||||
)
|
||||
|
||||
|
||||
@ -125,6 +125,49 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
@admin.register(ObjectChange)
|
||||
class ObjectChangeAdmin(admin.ModelAdmin):
|
||||
actions = None
|
||||
fields = ['time', 'content_type', 'display_object', 'action', 'display_user']
|
||||
list_display = ['time', 'content_type', 'display_object', 'display_action', 'display_user']
|
||||
list_filter = ['time', 'action', 'user__username']
|
||||
list_select_related = ['content_type', 'user']
|
||||
readonly_fields = fields
|
||||
search_fields = ['user_name', 'object_repr']
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def display_user(self, obj):
|
||||
if obj.user is not None:
|
||||
return obj.user
|
||||
else:
|
||||
return '{} (deleted)'.format(obj.user_name)
|
||||
display_user.short_description = 'user'
|
||||
|
||||
def display_action(self, obj):
|
||||
icon = {
|
||||
OBJECTCHANGE_ACTION_CREATE: 'addlink',
|
||||
OBJECTCHANGE_ACTION_UPDATE: 'changelink',
|
||||
OBJECTCHANGE_ACTION_DELETE: 'deletelink',
|
||||
}
|
||||
return mark_safe('<span class="{}">{}</span>'.format(icon[obj.action], obj.get_action_display()))
|
||||
display_user.short_description = 'action'
|
||||
|
||||
def display_object(self, obj):
|
||||
if hasattr(obj.changed_object, 'get_absolute_url'):
|
||||
return mark_safe('<a href="{}">{}</a>'.format(obj.changed_object.get_absolute_url(), obj.changed_object))
|
||||
elif obj.changed_object is not None:
|
||||
return obj.changed_object
|
||||
else:
|
||||
return '{} (deleted)'.format(obj.object_repr)
|
||||
display_object.short_description = 'object'
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
@ -66,6 +66,16 @@ TOPOLOGYMAP_TYPE_CHOICES = (
|
||||
(TOPOLOGYMAP_TYPE_POWER, 'Power'),
|
||||
)
|
||||
|
||||
# Change log actions
|
||||
OBJECTCHANGE_ACTION_CREATE = 1
|
||||
OBJECTCHANGE_ACTION_UPDATE = 2
|
||||
OBJECTCHANGE_ACTION_DELETE = 3
|
||||
OBJECTCHANGE_ACTION_CHOICES = (
|
||||
(OBJECTCHANGE_ACTION_CREATE, 'Created'),
|
||||
(OBJECTCHANGE_ACTION_UPDATE, 'Updated'),
|
||||
(OBJECTCHANGE_ACTION_DELETE, 'Deleted'),
|
||||
)
|
||||
|
||||
# User action types
|
||||
ACTION_CREATE = 1
|
||||
ACTION_IMPORT = 2
|
||||
|
63
netbox/extras/middleware.py
Normal file
63
netbox/extras/middleware.py
Normal file
@ -0,0 +1,63 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from django.core.serializers import serialize
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.utils.functional import curry, SimpleLazyObject
|
||||
|
||||
from utilities.models import ChangeLoggedModel
|
||||
from .constants import OBJECTCHANGE_ACTION_CREATE, OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE
|
||||
from .models import ObjectChange
|
||||
|
||||
|
||||
def record_object_change(user, instance, **kwargs):
|
||||
"""
|
||||
Create an ObjectChange in response to an object being created or deleted.
|
||||
"""
|
||||
if not isinstance(instance, ChangeLoggedModel):
|
||||
return
|
||||
|
||||
# Determine what action is being performed. The post_save signal sends a `created` boolean, whereas post_delete
|
||||
# does not.
|
||||
if 'created' in kwargs:
|
||||
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
|
||||
else:
|
||||
action = OBJECTCHANGE_ACTION_DELETE
|
||||
|
||||
# Serialize the object using Django's built-in JSON serializer, then extract only the `fields` dict.
|
||||
json_str = serialize('json', [instance])
|
||||
object_data = json.loads(json_str)[0]['fields']
|
||||
|
||||
ObjectChange(
|
||||
user=user,
|
||||
changed_object=instance,
|
||||
action=action,
|
||||
object_data=object_data
|
||||
).save()
|
||||
|
||||
|
||||
class ChangeLoggingMiddleware(object):
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
def get_user(request):
|
||||
return request.user
|
||||
|
||||
# DRF employs a separate authentication mechanism outside Django's normal request/response cycle, so calling
|
||||
# request.user in middleware will always return AnonymousUser for API requests. To work around this, we point
|
||||
# to a lazy object that doesn't resolve the user until after DRF's authentication has been called. For more
|
||||
# detail, see https://stackoverflow.com/questions/26240832/
|
||||
user = SimpleLazyObject(lambda: get_user(request))
|
||||
|
||||
# Django doesn't provide any request context with the post_save/post_delete signals, so we curry
|
||||
# record_object_change() to include the user associated with the current request.
|
||||
_record_object_change = curry(record_object_change, user)
|
||||
|
||||
post_save.connect(_record_object_change)
|
||||
post_delete.connect(_record_object_change)
|
||||
|
||||
return self.get_response(request)
|
37
netbox/extras/migrations/0013_objectchange.py
Normal file
37
netbox/extras/migrations/0013_objectchange.py
Normal file
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-06-13 20:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('extras', '0012_webhooks'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ObjectChange',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('user_name', models.CharField(editable=False, max_length=150)),
|
||||
('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('object_repr', models.CharField(editable=False, max_length=200)),
|
||||
('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time'],
|
||||
},
|
||||
),
|
||||
]
|
@ -656,6 +656,71 @@ class ReportResult(models.Model):
|
||||
ordering = ['report']
|
||||
|
||||
|
||||
#
|
||||
# Change logging
|
||||
#
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ObjectChange(models.Model):
|
||||
"""
|
||||
Record a change to an object and the user account associated with that change.
|
||||
"""
|
||||
time = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
editable=False
|
||||
)
|
||||
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
|
||||
)
|
||||
action = models.PositiveSmallIntegerField(
|
||||
choices=OBJECTCHANGE_ACTION_CHOICES
|
||||
)
|
||||
content_type = models.ForeignKey(
|
||||
to=ContentType,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
object_id = models.PositiveIntegerField()
|
||||
changed_object = GenericForeignKey(
|
||||
ct_field='content_type',
|
||||
fk_field='object_id'
|
||||
)
|
||||
object_repr = models.CharField(
|
||||
max_length=200,
|
||||
editable=False
|
||||
)
|
||||
object_data = JSONField(
|
||||
editable=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-time']
|
||||
|
||||
def __str__(self):
|
||||
attribution = 'by {}'.format(self.user_name) if self.user_name else '(no attribution)'
|
||||
return '{} {} {}'.format(
|
||||
self.object_repr,
|
||||
self.get_action_display().lower(),
|
||||
attribution
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Record the user's name and the object's representation as static strings
|
||||
if self.user is not None:
|
||||
self.user_name = self.user.username
|
||||
self.object_repr = str(self.changed_object)
|
||||
|
||||
return super(ObjectChange, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# User actions
|
||||
#
|
||||
|
@ -174,6 +174,7 @@ MIDDLEWARE = (
|
||||
'utilities.middleware.ExceptionHandlingMiddleware',
|
||||
'utilities.middleware.LoginRequiredMiddleware',
|
||||
'utilities.middleware.APIVersionMiddleware',
|
||||
'extras.middleware.ChangeLoggingMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'netbox.urls'
|
||||
|
Reference in New Issue
Block a user