Parcourir la source

Implemented object add/edit/delete logging

Jeremy Stretch il y a 9 ans
Parent
commit
cb8e0c93f2

+ 2 - 5
netbox/circuits/views.py

@@ -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):

+ 3 - 6
netbox/dcim/views.py

@@ -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):

+ 6 - 1
netbox/extras/admin.py

@@ -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 - 0
netbox/extras/migrations/0004_useraction.py

@@ -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'],
+            },
+        ),
+    ]

+ 73 - 0
netbox/extras/models.py

@@ -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']

+ 5 - 11
netbox/ipam/views.py

@@ -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):

+ 1 - 2
netbox/secrets/views.py

@@ -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):

+ 22 - 7
netbox/utilities/views.py

@@ -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: