mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Implemented object add/edit/delete logging
This commit is contained in:
@ -1,4 +1,3 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
@ -69,8 +68,7 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} providers".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -165,8 +163,7 @@ class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} circuits".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
@ -200,8 +200,7 @@ class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} racks".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -305,8 +304,7 @@ class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} device types".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -590,8 +588,7 @@ class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} devices".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Graph, ExportTemplate, TopologyMap
|
||||
from .models import Graph, ExportTemplate, TopologyMap, UserAction
|
||||
|
||||
|
||||
@admin.register(Graph)
|
||||
@ -19,3 +19,8 @@ class TopologyMapAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {
|
||||
'slug': ['name'],
|
||||
}
|
||||
|
||||
|
||||
@admin.register(UserAction)
|
||||
class UserActionAdmin(admin.ModelAdmin):
|
||||
list_display = ['user', 'action', 'content_type', 'object_id', 'message']
|
||||
|
34
netbox/extras/migrations/0004_useraction.py
Normal file
34
netbox/extras/migrations/0004_useraction.py
Normal file
@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.5 on 2016-05-23 18:16
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
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', '0003_auto_20160412_1332'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserAction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('action', models.PositiveSmallIntegerField(choices=[(1, b'created'), (2, b'imported'), (3, b'modified'), (4, b'bulk edited'), (5, b'deleted'), (6, b'bulk deleted')])),
|
||||
('message', models.TextField(blank=True)),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time'],
|
||||
},
|
||||
),
|
||||
]
|
@ -1,3 +1,4 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
@ -21,6 +22,21 @@ EXPORTTEMPLATE_MODELS = [
|
||||
'provider', 'circuit'
|
||||
]
|
||||
|
||||
ACTION_CREATE = 1
|
||||
ACTION_IMPORT = 2
|
||||
ACTION_EDIT = 3
|
||||
ACTION_BULK_EDIT = 4
|
||||
ACTION_DELETE = 5
|
||||
ACTION_BULK_DELETE = 6
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_CREATE, 'created'),
|
||||
(ACTION_IMPORT, 'imported'),
|
||||
(ACTION_EDIT, 'modified'),
|
||||
(ACTION_BULK_EDIT, 'bulk edited'),
|
||||
(ACTION_DELETE, 'deleted'),
|
||||
(ACTION_BULK_DELETE, 'bulk deleted')
|
||||
)
|
||||
|
||||
|
||||
class Graph(models.Model):
|
||||
type = models.PositiveSmallIntegerField(choices=GRAPH_TYPE_CHOICES)
|
||||
@ -93,3 +109,60 @@ class TopologyMap(models.Model):
|
||||
if not self.device_patterns:
|
||||
return None
|
||||
return [line.strip() for line in self.device_patterns.split('\n')]
|
||||
|
||||
|
||||
class UserActionManager(models.Manager):
|
||||
|
||||
# Actions affecting a single object
|
||||
def log_action(self, user, obj, action, message):
|
||||
self.model.objects.create(
|
||||
content_type = ContentType.objects.get_for_model(obj),
|
||||
object_id = obj.pk,
|
||||
user = user,
|
||||
action = action,
|
||||
message = message,
|
||||
)
|
||||
|
||||
def log_create(self, user, obj, message=''):
|
||||
self.log_action(user, obj, ACTION_CREATE, message)
|
||||
|
||||
def log_edit(self, user, obj, message=''):
|
||||
self.log_action(user, obj, ACTION_EDIT, message)
|
||||
|
||||
def log_delete(self, user, obj, message=''):
|
||||
self.log_action(user, obj, ACTION_DELETE, message)
|
||||
|
||||
# Actions affecting multiple objects
|
||||
def log_bulk_action(self, user, content_type, action, message):
|
||||
self.model.objects.create(
|
||||
content_type=content_type,
|
||||
user=user,
|
||||
action=action,
|
||||
message=message,
|
||||
)
|
||||
|
||||
def log_import(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_IMPORT, message)
|
||||
|
||||
def log_bulk_edit(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_EDIT, message)
|
||||
|
||||
def log_bulk_delete(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_DELETE, message)
|
||||
|
||||
|
||||
class UserAction(models.Model):
|
||||
"""
|
||||
A record of an action (add, edit, or delete) performed on an object by a User.
|
||||
"""
|
||||
time = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField(blank=True, null=True)
|
||||
action = models.PositiveSmallIntegerField(choices=ACTION_CHOICES)
|
||||
message = models.TextField(blank=True)
|
||||
|
||||
objects = UserActionManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['-time']
|
||||
|
@ -1,7 +1,6 @@
|
||||
from netaddr import IPSet
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
@ -90,8 +89,7 @@ class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} VRFs".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -198,8 +196,7 @@ class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} aggregates".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -336,8 +333,7 @@ class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} prefixes".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -449,8 +445,7 @@ class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} IP addresses".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
@ -519,8 +514,7 @@ class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} VLANs".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
@ -213,8 +213,7 @@ class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
|
||||
if form.cleaned_data[field]:
|
||||
fields_to_update[field] = form.cleaned_data[field]
|
||||
|
||||
updated_count = self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
messages.success(self.request, "Updated {} secrets".format(updated_count))
|
||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||
|
||||
|
||||
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
|
@ -13,7 +13,7 @@ from django.utils.decorators import method_decorator
|
||||
from django.utils.http import is_safe_url
|
||||
from django.views.generic import View
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from extras.models import ExportTemplate, UserAction
|
||||
|
||||
from .error_handlers import handle_protectederror
|
||||
from .forms import ConfirmationForm
|
||||
@ -116,6 +116,7 @@ class ObjectEditView(View):
|
||||
obj = form.save(commit=False)
|
||||
obj_created = not obj.pk
|
||||
obj.save()
|
||||
|
||||
msg = 'Created ' if obj_created else 'Modified '
|
||||
msg += self.model._meta.verbose_name
|
||||
if hasattr(obj, 'get_absolute_url'):
|
||||
@ -123,6 +124,11 @@ class ObjectEditView(View):
|
||||
else:
|
||||
msg += ' {}'.format(obj)
|
||||
messages.success(request, msg)
|
||||
if obj_created:
|
||||
UserAction.objects.log_create(request.user, obj, msg)
|
||||
else:
|
||||
UserAction.objects.log_edit(request.user, obj, msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
elif self.success_url:
|
||||
@ -169,7 +175,9 @@ class ObjectDeleteView(View):
|
||||
if form.is_valid():
|
||||
try:
|
||||
obj.delete()
|
||||
messages.success(request, 'Deleted {} {}'.format(self.model._meta.verbose_name, obj))
|
||||
msg = 'Deleted {} {}'.format(self.model._meta.verbose_name, obj)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_delete(request.user, obj, msg)
|
||||
return redirect(self.redirect_url)
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(obj, request, e)
|
||||
@ -208,7 +216,9 @@ class BulkImportView(View):
|
||||
new_objs.append(obj)
|
||||
|
||||
obj_table = self.table(new_objs)
|
||||
messages.success(request, "Imported {} objects".format(len(new_objs)))
|
||||
msg = 'Imported {} objects'.format(len(new_objs))
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_import(request.user, ContentType.objects.get_for_model(new_objs[0]), msg)
|
||||
|
||||
return render(request, "import_success.html", {
|
||||
'table': obj_table,
|
||||
@ -247,9 +257,12 @@ class BulkEditView(View):
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
pk_list = [obj.pk for obj in form.cleaned_data['pk']]
|
||||
self.update_objects(pk_list, form)
|
||||
if not form.errors:
|
||||
return redirect(redirect_url)
|
||||
updated_count = self.update_objects(pk_list, form)
|
||||
msg = 'Updated {} {}'.format(updated_count, self.cls._meta.verbose_name_plural)
|
||||
messages.success(self.request, msg)
|
||||
UserAction.objects.log_bulk_edit(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
||||
|
||||
return redirect(redirect_url)
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
||||
@ -306,7 +319,9 @@ class BulkDeleteView(View):
|
||||
handle_protectederror(list(objects_to_delete), request, e)
|
||||
return redirect(redirect_url)
|
||||
|
||||
messages.success(request, "Deleted {} {}".format(deleted_count, self.cls._meta.verbose_name_plural))
|
||||
msg = 'Deleted {} {}'.format(deleted_count, self.cls._meta.verbose_name_plural)
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_bulk_delete(request.user, ContentType.objects.get_for_model(self.cls), msg)
|
||||
return redirect(redirect_url)
|
||||
|
||||
else:
|
||||
|
Reference in New Issue
Block a user