mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Extend ObjectChange to optionally indicate a related object (e.g. a parent device)
This commit is contained in:
@ -18,16 +18,41 @@ from taggit.managers import TaggableManager
|
|||||||
from timezone_field import TimeZoneField
|
from timezone_field import TimeZoneField
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.models import CustomFieldModel
|
from extras.models import CustomFieldModel, ObjectChange
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
from utilities.fields import ColorField, NullableCharField
|
from utilities.fields import ColorField, NullableCharField
|
||||||
from utilities.managers import NaturalOrderByManager
|
from utilities.managers import NaturalOrderByManager
|
||||||
from utilities.models import ChangeLoggedModel
|
from utilities.models import ChangeLoggedModel
|
||||||
|
from utilities.utils import serialize_object
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .fields import ASNField, MACAddressField
|
from .fields import ASNField, MACAddressField
|
||||||
from .querysets import InterfaceQuerySet
|
from .querysets import InterfaceQuerySet
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentModel(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"ComponentModel must implement get_component_parent()"
|
||||||
|
)
|
||||||
|
|
||||||
|
def log_change(self, user, request_id, action):
|
||||||
|
"""
|
||||||
|
Log an ObjectChange including the parent Device.
|
||||||
|
"""
|
||||||
|
ObjectChange(
|
||||||
|
user=user,
|
||||||
|
request_id=request_id,
|
||||||
|
changed_object=self,
|
||||||
|
related_object=self.get_component_parent(),
|
||||||
|
action=action,
|
||||||
|
object_data=serialize_object(self)
|
||||||
|
).save()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
@ -866,7 +891,7 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
|
|||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ConsolePortTemplate(models.Model):
|
class ConsolePortTemplate(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A template for a ConsolePort to be created for a new Device.
|
A template for a ConsolePort to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
@ -886,9 +911,12 @@ class ConsolePortTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device_type
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ConsoleServerPortTemplate(models.Model):
|
class ConsoleServerPortTemplate(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A template for a ConsoleServerPort to be created for a new Device.
|
A template for a ConsoleServerPort to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
@ -908,9 +936,12 @@ class ConsoleServerPortTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device_type
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class PowerPortTemplate(models.Model):
|
class PowerPortTemplate(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A template for a PowerPort to be created for a new Device.
|
A template for a PowerPort to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
@ -930,9 +961,12 @@ class PowerPortTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device_type
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class PowerOutletTemplate(models.Model):
|
class PowerOutletTemplate(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A template for a PowerOutlet to be created for a new Device.
|
A template for a PowerOutlet to be created for a new Device.
|
||||||
"""
|
"""
|
||||||
@ -952,9 +986,12 @@ class PowerOutletTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device_type
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class InterfaceTemplate(models.Model):
|
class InterfaceTemplate(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A template for a physical data interface on a new Device.
|
A template for a physical data interface on a new Device.
|
||||||
"""
|
"""
|
||||||
@ -984,9 +1021,12 @@ class InterfaceTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device_type
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class DeviceBayTemplate(models.Model):
|
class DeviceBayTemplate(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A template for a DeviceBay to be created for a new parent Device.
|
A template for a DeviceBay to be created for a new parent Device.
|
||||||
"""
|
"""
|
||||||
@ -1006,6 +1046,9 @@ class DeviceBayTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device_type
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Devices
|
# Devices
|
||||||
@ -1502,7 +1545,7 @@ class Device(ChangeLoggedModel, CustomFieldModel):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ConsolePort(models.Model):
|
class ConsolePort(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
"""
|
"""
|
||||||
@ -1539,6 +1582,9 @@ class ConsolePort(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.device.get_absolute_url()
|
return self.device.get_absolute_url()
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.cs_port.device.identifier if self.cs_port else None,
|
self.cs_port.device.identifier if self.cs_port else None,
|
||||||
@ -1564,7 +1610,7 @@ class ConsoleServerPortManager(models.Manager):
|
|||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ConsoleServerPort(models.Model):
|
class ConsoleServerPort(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
"""
|
"""
|
||||||
@ -1588,6 +1634,9 @@ class ConsoleServerPort(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.device.get_absolute_url()
|
return self.device.get_absolute_url()
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Check that the parent device's DeviceType is a console server
|
# Check that the parent device's DeviceType is a console server
|
||||||
@ -1605,7 +1654,7 @@ class ConsoleServerPort(models.Model):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class PowerPort(models.Model):
|
class PowerPort(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
"""
|
"""
|
||||||
@ -1641,6 +1690,9 @@ class PowerPort(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.device.get_absolute_url()
|
return self.device.get_absolute_url()
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.power_outlet.device.identifier if self.power_outlet else None,
|
self.power_outlet.device.identifier if self.power_outlet else None,
|
||||||
@ -1666,7 +1718,7 @@ class PowerOutletManager(models.Manager):
|
|||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class PowerOutlet(models.Model):
|
class PowerOutlet(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
"""
|
"""
|
||||||
@ -1690,6 +1742,9 @@ class PowerOutlet(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.device.get_absolute_url()
|
return self.device.get_absolute_url()
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Check that the parent device's DeviceType is a PDU
|
# Check that the parent device's DeviceType is a PDU
|
||||||
@ -1707,7 +1762,7 @@ class PowerOutlet(models.Model):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Interface(models.Model):
|
class Interface(ComponentModel):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
|
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
|
||||||
Interface via the creation of an InterfaceConnection.
|
Interface via the creation of an InterfaceConnection.
|
||||||
@ -1797,6 +1852,9 @@ class Interface(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.parent.get_absolute_url()
|
return self.parent.get_absolute_url()
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device or self.virtual_machine
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Check that the parent device's DeviceType is a network device
|
# Check that the parent device's DeviceType is a network device
|
||||||
@ -1867,6 +1925,7 @@ class Interface(models.Model):
|
|||||||
|
|
||||||
return super(Interface, self).save(*args, **kwargs)
|
return super(Interface, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
# TODO: Replace `parent` with get_component_parent() (from ComponentModel)
|
||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
return self.device or self.virtual_machine
|
return self.device or self.virtual_machine
|
||||||
@ -1977,7 +2036,7 @@ class InterfaceConnection(models.Model):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class DeviceBay(models.Model):
|
class DeviceBay(ComponentModel):
|
||||||
"""
|
"""
|
||||||
An empty space within a Device which can house a child device
|
An empty space within a Device which can house a child device
|
||||||
"""
|
"""
|
||||||
@ -2008,6 +2067,9 @@ class DeviceBay(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.device.get_absolute_url()
|
return self.device.get_absolute_url()
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# Validate that the parent Device can have DeviceBays
|
# Validate that the parent Device can have DeviceBays
|
||||||
@ -2026,7 +2088,7 @@ class DeviceBay(models.Model):
|
|||||||
#
|
#
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class InventoryItem(models.Model):
|
class InventoryItem(ComponentModel):
|
||||||
"""
|
"""
|
||||||
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
|
||||||
InventoryItems are used only for inventory purposes.
|
InventoryItems are used only for inventory purposes.
|
||||||
@ -2095,6 +2157,9 @@ class InventoryItem(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.device.get_absolute_url()
|
return self.device.get_absolute_url()
|
||||||
|
|
||||||
|
def get_component_parent(self):
|
||||||
|
return self.device
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.device.name or '{' + self.device.pk + '}',
|
self.device.name or '{' + self.device.pk + '}',
|
||||||
|
@ -132,10 +132,10 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(ObjectChange)
|
@admin.register(ObjectChange)
|
||||||
class ObjectChangeAdmin(admin.ModelAdmin):
|
class ObjectChangeAdmin(admin.ModelAdmin):
|
||||||
actions = None
|
actions = None
|
||||||
fields = ['time', 'content_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
|
fields = ['time', 'changed_object_type', 'display_object', 'action', 'display_user', 'request_id', 'object_data']
|
||||||
list_display = ['time', 'content_type', 'display_object', 'display_action', 'display_user', 'request_id']
|
list_display = ['time', 'changed_object_type', 'display_object', 'display_action', 'display_user', 'request_id']
|
||||||
list_filter = ['time', 'action', 'user__username']
|
list_filter = ['time', 'action', 'user__username']
|
||||||
list_select_related = ['content_type', 'user']
|
list_select_related = ['changed_object_type', 'user']
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
search_fields = ['user_name', 'object_repr', 'request_id']
|
search_fields = ['user_name', 'object_repr', 'request_id']
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ class ObjectChangeFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ObjectChange
|
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):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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 __future__ import unicode_literals
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -11,8 +11,8 @@ import django.db.models.deletion
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
('extras', '0012_webhooks'),
|
('extras', '0012_webhooks'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -25,10 +25,12 @@ class Migration(migrations.Migration):
|
|||||||
('user_name', models.CharField(editable=False, max_length=150)),
|
('user_name', models.CharField(editable=False, max_length=150)),
|
||||||
('request_id', models.UUIDField(editable=False)),
|
('request_id', models.UUIDField(editable=False)),
|
||||||
('action', models.PositiveSmallIntegerField(choices=[(1, 'Created'), (2, 'Updated'), (3, 'Deleted')])),
|
('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_repr', models.CharField(editable=False, max_length=200)),
|
||||||
('object_data', django.contrib.postgres.fields.jsonb.JSONField(editable=False)),
|
('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)),
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='changes', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
@ -665,7 +665,9 @@ class ReportResult(models.Model):
|
|||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ObjectChange(models.Model):
|
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(
|
time = models.DateTimeField(
|
||||||
auto_now_add=True,
|
auto_now_add=True,
|
||||||
@ -688,14 +690,30 @@ class ObjectChange(models.Model):
|
|||||||
action = models.PositiveSmallIntegerField(
|
action = models.PositiveSmallIntegerField(
|
||||||
choices=OBJECTCHANGE_ACTION_CHOICES
|
choices=OBJECTCHANGE_ACTION_CHOICES
|
||||||
)
|
)
|
||||||
content_type = models.ForeignKey(
|
changed_object_type = models.ForeignKey(
|
||||||
to=ContentType,
|
to=ContentType,
|
||||||
on_delete=models.CASCADE
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
)
|
)
|
||||||
object_id = models.PositiveIntegerField()
|
changed_object_id = models.PositiveIntegerField()
|
||||||
changed_object = GenericForeignKey(
|
changed_object = GenericForeignKey(
|
||||||
ct_field='content_type',
|
ct_field='changed_object_type',
|
||||||
fk_field='object_id'
|
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(
|
object_repr = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
@ -706,14 +724,17 @@ class ObjectChange(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
serializer = 'extras.api.serializers.ObjectChangeSerializer'
|
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:
|
class Meta:
|
||||||
ordering = ['-time']
|
ordering = ['-time']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} {} {} by {}'.format(
|
return '{} {} {} by {}'.format(
|
||||||
self.content_type,
|
self.changed_object_type,
|
||||||
self.object_repr,
|
self.object_repr,
|
||||||
self.get_action_display().lower(),
|
self.get_action_display().lower(),
|
||||||
self.user_name
|
self.user_name
|
||||||
@ -722,8 +743,7 @@ class ObjectChange(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
# Record the user's name and the object's representation as static strings
|
# 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)
|
self.object_repr = str(self.changed_object)
|
||||||
|
|
||||||
return super(ObjectChange, self).save(*args, **kwargs)
|
return super(ObjectChange, self).save(*args, **kwargs)
|
||||||
@ -734,11 +754,14 @@ class ObjectChange(models.Model):
|
|||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.time,
|
self.time,
|
||||||
self.user or self.user_name,
|
self.user,
|
||||||
|
self.user_name,
|
||||||
self.request_id,
|
self.request_id,
|
||||||
self.get_action_display(),
|
self.get_action_display(),
|
||||||
self.content_type,
|
self.changed_object_type,
|
||||||
self.object_id,
|
self.changed_object_id,
|
||||||
|
self.related_object_type,
|
||||||
|
self.related_object_id,
|
||||||
self.object_repr,
|
self.object_repr,
|
||||||
self.object_data,
|
self.object_data,
|
||||||
)
|
)
|
||||||
|
@ -52,6 +52,9 @@ class ObjectChangeTable(BaseTable):
|
|||||||
action = tables.TemplateColumn(
|
action = tables.TemplateColumn(
|
||||||
template_code=OBJECTCHANGE_ACTION
|
template_code=OBJECTCHANGE_ACTION
|
||||||
)
|
)
|
||||||
|
changed_object_type = tables.Column(
|
||||||
|
verbose_name='Type'
|
||||||
|
)
|
||||||
object_repr = tables.TemplateColumn(
|
object_repr = tables.TemplateColumn(
|
||||||
template_code=OBJECTCHANGE_OBJECT,
|
template_code=OBJECTCHANGE_OBJECT,
|
||||||
verbose_name='Object'
|
verbose_name='Object'
|
||||||
@ -62,4 +65,4 @@ class ObjectChangeTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = ObjectChange
|
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')
|
||||||
|
@ -4,7 +4,7 @@ from django import template
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
@ -94,13 +94,13 @@ class ObjectChangeLogView(View):
|
|||||||
# Get object my model and kwargs (e.g. slug='foo')
|
# Get object my model and kwargs (e.g. slug='foo')
|
||||||
obj = get_object_or_404(model, **kwargs)
|
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)
|
content_type = ContentType.objects.get_for_model(model)
|
||||||
objectchanges = ObjectChange.objects.select_related(
|
objectchanges = ObjectChange.objects.select_related(
|
||||||
'user', 'content_type'
|
'user', 'changed_object_type'
|
||||||
).filter(
|
).filter(
|
||||||
content_type=content_type,
|
Q(changed_object_type=content_type, changed_object_id=obj.pk) |
|
||||||
object_id=obj.pk
|
Q(related_object_type=content_type, related_object_id=obj.pk)
|
||||||
)
|
)
|
||||||
objectchanges_table = ObjectChangeTable(
|
objectchanges_table = ObjectChangeTable(
|
||||||
data=objectchanges,
|
data=objectchanges,
|
||||||
|
@ -53,9 +53,9 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Content Type</td>
|
<td>Object Type</td>
|
||||||
<td>
|
<td>
|
||||||
{{ objectchange.content_type }}
|
{{ objectchange.changed_object_type }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
Reference in New Issue
Block a user