Extend ObjectChange to optionally indicate a related object (e.g. a parent device)

This commit is contained in:
Jeremy Stretch
2018-06-22 15:05:40 -04:00
parent e3f625cb7b
commit 895bae649f
8 changed files with 136 additions and 43 deletions

View File

@@ -132,10 +132,10 @@ class TopologyMapAdmin(admin.ModelAdmin):
@admin.register(ObjectChange)
class ObjectChangeAdmin(admin.ModelAdmin):
actions = None
fields = ['time', 'content_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
list_display = ['time', 'content_type', 'display_object', 'display_action', 'display_user', 'request_id']
fields = ['time', 'changed_object_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
list_display = ['time', 'changed_object_type', 'display_object', 'display_action', 'display_user', 'request_id']
list_filter = ['time', 'action', 'user__username']
list_select_related = ['content_type', 'user']
list_select_related = ['changed_object_type', 'user']
readonly_fields = fields
search_fields = ['user_name', 'object_repr', 'request_id']

View File

@@ -133,7 +133,7 @@ class ObjectChangeFilter(django_filters.FilterSet):
class Meta:
model = ObjectChange
fields = ['user', 'user_name', 'request_id', 'action', 'content_type', 'object_repr']
fields = ['user', 'user_name', 'request_id', 'action', 'changed_object_type', 'object_repr']
def search(self, queryset, name, value):
if not value.strip():

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-06-19 19:34
# Generated by Django 1.11.12 on 2018-06-22 18:13
from __future__ import unicode_literals
from django.conf import settings
@@ -11,8 +11,8 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('extras', '0012_webhooks'),
]
@@ -25,10 +25,12 @@ class Migration(migrations.Migration):
('user_name', models.CharField(editable=False, max_length=150)),
('request_id', models.UUIDField(editable=False)),
('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
('object_id', models.PositiveIntegerField()),
('changed_object_id', models.PositiveIntegerField()),
('related_object_id', models.PositiveIntegerField(blank=True, null=True)),
('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')),
('changed_object_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.ContentType')),
('related_object_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', 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={

View File

@@ -665,7 +665,9 @@ class ReportResult(models.Model):
@python_2_unicode_compatible
class ObjectChange(models.Model):
"""
Record a change to an object and the user account associated with that change.
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,
@@ -688,14 +690,30 @@ class ObjectChange(models.Model):
action = models.PositiveSmallIntegerField(
choices=OBJECTCHANGE_ACTION_CHOICES
)
content_type = models.ForeignKey(
changed_object_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE
on_delete=models.PROTECT,
related_name='+'
)
object_id = models.PositiveIntegerField()
changed_object_id = models.PositiveIntegerField()
changed_object = GenericForeignKey(
ct_field='content_type',
fk_field='object_id'
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,
@@ -706,14 +724,17 @@ class ObjectChange(models.Model):
)
serializer = 'extras.api.serializers.ObjectChangeSerializer'
csv_headers = ['time', 'user', 'request_id', 'action', 'content_type', 'object_id', 'object_repr', 'object_data']
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.content_type,
self.changed_object_type,
self.object_repr,
self.get_action_display().lower(),
self.user_name
@@ -722,8 +743,7 @@ class ObjectChange(models.Model):
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.user_name = self.user.username
self.object_repr = str(self.changed_object)
return super(ObjectChange, self).save(*args, **kwargs)
@@ -734,11 +754,14 @@ class ObjectChange(models.Model):
def to_csv(self):
return (
self.time,
self.user or self.user_name,
self.user,
self.user_name,
self.request_id,
self.get_action_display(),
self.content_type,
self.object_id,
self.changed_object_type,
self.changed_object_id,
self.related_object_type,
self.related_object_id,
self.object_repr,
self.object_data,
)

View File

@@ -52,6 +52,9 @@ class ObjectChangeTable(BaseTable):
action = tables.TemplateColumn(
template_code=OBJECTCHANGE_ACTION
)
changed_object_type = tables.Column(
verbose_name='Type'
)
object_repr = tables.TemplateColumn(
template_code=OBJECTCHANGE_OBJECT,
verbose_name='Object'
@@ -62,4 +65,4 @@ class ObjectChangeTable(BaseTable):
class Meta(BaseTable.Meta):
model = ObjectChange
fields = ('time', 'user_name', 'action', 'content_type', 'object_repr', 'request_id')
fields = ('time', 'user_name', 'action', 'changed_object_type', 'object_repr', 'request_id')

View File

@@ -4,7 +4,7 @@ from django import template
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.db.models import Count, Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.safestring import mark_safe
@@ -94,13 +94,13 @@ class ObjectChangeLogView(View):
# Get object my model and kwargs (e.g. slug='foo')
obj = get_object_or_404(model, **kwargs)
# Gather all changes for this object
# Gather all changes for this object (and its related objects)
content_type = ContentType.objects.get_for_model(model)
objectchanges = ObjectChange.objects.select_related(
'user', 'content_type'
'user', 'changed_object_type'
).filter(
content_type=content_type,
object_id=obj.pk
Q(changed_object_type=content_type, changed_object_id=obj.pk) |
Q(related_object_type=content_type, related_object_id=obj.pk)
)
objectchanges_table = ObjectChangeTable(
data=objectchanges,