Explorar el Código

Added changelog views

Jeremy Stretch hace 7 años
padre
commit
3c2e0b0b17

+ 5 - 0
netbox/circuits/urls.py

@@ -2,7 +2,9 @@ from __future__ import unicode_literals
 
 
 from django.conf.urls import url
 from django.conf.urls import url
 
 
+from extras.views import ChangeLogView
 from . import views
 from . import views
+from .models import Circuit, CircuitType, Provider
 
 
 app_name = 'circuits'
 app_name = 'circuits'
 urlpatterns = [
 urlpatterns = [
@@ -16,6 +18,7 @@ urlpatterns = [
     url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
     url(r'^providers/(?P<slug>[\w-]+)/$', views.ProviderView.as_view(), name='provider'),
     url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
     url(r'^providers/(?P<slug>[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'),
     url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
     url(r'^providers/(?P<slug>[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'),
+    url(r'^providers/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
 
 
     # Circuit types
     # Circuit types
     url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
     url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
@@ -23,6 +26,7 @@ urlpatterns = [
     url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
     url(r'^circuit-types/import/$', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
     url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
     url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
     url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
     url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
+    url(r'^circuit-types/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
 
 
     # Circuits
     # Circuits
     url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
     url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
@@ -33,6 +37,7 @@ urlpatterns = [
     url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
     url(r'^circuits/(?P<pk>\d+)/$', views.CircuitView.as_view(), name='circuit'),
     url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
     url(r'^circuits/(?P<pk>\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'),
     url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
     url(r'^circuits/(?P<pk>\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'),
+    url(r'^circuits/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
     url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
     url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
 
 
     # Circuit terminations
     # Circuit terminations

+ 17 - 2
netbox/dcim/urls.py

@@ -2,11 +2,14 @@ from __future__ import unicode_literals
 
 
 from django.conf.urls import url
 from django.conf.urls import url
 
 
-from extras.views import ImageAttachmentEditView
+from extras.views import ChangeLogView, ImageAttachmentEditView
 from ipam.views import ServiceCreateView
 from ipam.views import ServiceCreateView
 from secrets.views import secret_add
 from secrets.views import secret_add
 from . import views
 from . import views
-from .models import Device, Rack, Site
+from .models import (
+    Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackReservation, RackRole, Region, Site,
+    VirtualChassis,
+)
 
 
 app_name = 'dcim'
 app_name = 'dcim'
 urlpatterns = [
 urlpatterns = [
@@ -17,6 +20,7 @@ urlpatterns = [
     url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
     url(r'^regions/import/$', views.RegionBulkImportView.as_view(), name='region_import'),
     url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
     url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
     url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
     url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
+    url(r'^regions/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}),
 
 
     # Sites
     # Sites
     url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
     url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
@@ -26,6 +30,7 @@ urlpatterns = [
     url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
     url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
     url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
     url(r'^sites/(?P<slug>[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'),
     url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
     url(r'^sites/(?P<slug>[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'),
+    url(r'^sites/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
     url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
     url(r'^sites/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}),
 
 
     # Rack groups
     # Rack groups
@@ -34,6 +39,7 @@ urlpatterns = [
     url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
     url(r'^rack-groups/import/$', views.RackGroupBulkImportView.as_view(), name='rackgroup_import'),
     url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
     url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
     url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
     url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
+    url(r'^rack-groups/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}),
 
 
     # Rack roles
     # Rack roles
     url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
     url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
@@ -41,6 +47,7 @@ urlpatterns = [
     url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
     url(r'^rack-roles/import/$', views.RackRoleBulkImportView.as_view(), name='rackrole_import'),
     url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
     url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
     url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
     url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
+    url(r'^rack-roles/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}),
 
 
     # Rack reservations
     # Rack reservations
     url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
     url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
@@ -48,6 +55,7 @@ urlpatterns = [
     url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
     url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
     url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
     url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
     url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
     url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
+    url(r'^rack-reservations/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}),
 
 
     # Racks
     # Racks
     url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
     url(r'^racks/$', views.RackListView.as_view(), name='rack_list'),
@@ -59,6 +67,7 @@ urlpatterns = [
     url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
     url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
     url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
     url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
     url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
     url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
+    url(r'^racks/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}),
     url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
     url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
     url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
     url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
 
 
@@ -68,6 +77,7 @@ urlpatterns = [
     url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
     url(r'^manufacturers/import/$', views.ManufacturerBulkImportView.as_view(), name='manufacturer_import'),
     url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
     url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
     url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
     url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
+    url(r'^manufacturers/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}),
 
 
     # Device types
     # Device types
     url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
     url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
@@ -78,6 +88,7 @@ urlpatterns = [
     url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
     url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
     url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
     url(r'^device-types/(?P<pk>\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'),
     url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
     url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
+    url(r'^device-types/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}),
 
 
     # Console port templates
     # Console port templates
     url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
     url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
@@ -110,6 +121,7 @@ urlpatterns = [
     url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
     url(r'^device-roles/import/$', views.DeviceRoleBulkImportView.as_view(), name='devicerole_import'),
     url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
     url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
     url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
     url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
+    url(r'^device-roles/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}),
 
 
     # Platforms
     # Platforms
     url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
     url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
@@ -117,6 +129,7 @@ urlpatterns = [
     url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
     url(r'^platforms/import/$', views.PlatformBulkImportView.as_view(), name='platform_import'),
     url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
     url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
     url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
     url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
+    url(r'^platforms/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}),
 
 
     # Devices
     # Devices
     url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
     url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
@@ -128,6 +141,7 @@ urlpatterns = [
     url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
     url(r'^devices/(?P<pk>\d+)/$', views.DeviceView.as_view(), name='device'),
     url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
     url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
     url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
     url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
+    url(r'^devices/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}),
     url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
     url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
     url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
     url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
     url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
     url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
@@ -221,6 +235,7 @@ urlpatterns = [
     url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
     url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'),
     url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
     url(r'^virtual-chassis/(?P<pk>\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'),
     url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
     url(r'^virtual-chassis/(?P<pk>\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'),
+    url(r'^virtual-chassis/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}),
     url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
     url(r'^virtual-chassis/(?P<pk>\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'),
     url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
     url(r'^virtual-chassis-members/(?P<pk>\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'),
 
 

+ 2 - 2
netbox/extras/admin.py

@@ -132,7 +132,7 @@ 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']
+    fields = ['time', 'content_type', 'display_object', 'action', 'display_user', 'object_data']
     list_display = ['time', 'content_type', 'display_object', 'display_action', 'display_user']
     list_display = ['time', 'content_type', 'display_object', 'display_action', 'display_user']
     list_filter = ['time', 'action', 'user__username']
     list_filter = ['time', 'action', 'user__username']
     list_select_related = ['content_type', 'user']
     list_select_related = ['content_type', 'user']
@@ -156,7 +156,7 @@ class ObjectChangeAdmin(admin.ModelAdmin):
             OBJECTCHANGE_ACTION_DELETE: 'deletelink',
             OBJECTCHANGE_ACTION_DELETE: 'deletelink',
         }
         }
         return mark_safe('<span class="{}">{}</span>'.format(icon[obj.action], obj.get_action_display()))
         return mark_safe('<span class="{}">{}</span>'.format(icon[obj.action], obj.get_action_display()))
-    display_user.short_description = 'action'
+    display_action.short_description = 'action'
 
 
     def display_object(self, obj):
     def display_object(self, obj):
         if hasattr(obj.changed_object, 'get_absolute_url'):
         if hasattr(obj.changed_object, 'get_absolute_url'):

+ 5 - 0
netbox/extras/models.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 
 from collections import OrderedDict
 from collections import OrderedDict
 from datetime import date
 from datetime import date
+import json
 
 
 import graphviz
 import graphviz
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
@@ -720,6 +721,10 @@ class ObjectChange(models.Model):
 
 
         return super(ObjectChange, self).save(*args, **kwargs)
         return super(ObjectChange, self).save(*args, **kwargs)
 
 
+    @property
+    def object_data_pretty(self):
+        return json.dumps(self.object_data, indent=4, sort_keys=True)
+
 
 
 #
 #
 # User actions
 # User actions

+ 38 - 1
netbox/extras/views.py

@@ -1,7 +1,9 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+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.db.models import Count
 from django.db.models import Count
 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
@@ -12,7 +14,7 @@ from taggit.models import Tag
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
 from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView
 from .forms import ImageAttachmentForm, TagForm
 from .forms import ImageAttachmentForm, TagForm
-from .models import ImageAttachment, ReportResult, UserAction
+from .models import ImageAttachment, ObjectChange, ReportResult, UserAction
 from .reports import get_report, get_reports
 from .reports import get_report, get_reports
 from .tables import TagTable
 from .tables import TagTable
 
 
@@ -50,6 +52,41 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
     default_return_url = 'extras:tag_list'
     default_return_url = 'extras:tag_list'
 
 
 
 
+#
+# Change logging
+#
+
+class ChangeLogView(View):
+    """
+    Present a history of changes made to an object.
+    """
+
+    def get(self, request, model, **kwargs):
+
+        # Get object my model and kwargs (e.g. slug='foo')
+        obj = get_object_or_404(model, **kwargs)
+
+        # Gather all changes for this object
+        content_type = ContentType.objects.get_for_model(model)
+        changes = ObjectChange.objects.filter(content_type=content_type, object_id=obj.pk)
+
+        # Check whether a header template exists for this model
+        base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
+        try:
+            template.loader.get_template(base_template)
+            object_var = model._meta.model_name
+        except template.TemplateDoesNotExist:
+            base_template = '_base.html'
+            object_var = 'obj'
+
+        return render(request, 'extras/changelog.html', {
+            object_var: obj,
+            'changes': changes,
+            'base_template': base_template,
+            'active_tab': 'changelog',
+        })
+
+
 #
 #
 # Image attachments
 # Image attachments
 #
 #

+ 11 - 0
netbox/ipam/urls.py

@@ -2,7 +2,9 @@ from __future__ import unicode_literals
 
 
 from django.conf.urls import url
 from django.conf.urls import url
 
 
+from extras.views import ChangeLogView
 from . import views
 from . import views
+from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 
 
 app_name = 'ipam'
 app_name = 'ipam'
@@ -17,6 +19,7 @@ urlpatterns = [
     url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
     url(r'^vrfs/(?P<pk>\d+)/$', views.VRFView.as_view(), name='vrf'),
     url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
     url(r'^vrfs/(?P<pk>\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'),
     url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
     url(r'^vrfs/(?P<pk>\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'),
+    url(r'^vrfs/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}),
 
 
     # RIRs
     # RIRs
     url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
     url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
@@ -24,6 +27,7 @@ urlpatterns = [
     url(r'^rirs/import/$', views.RIRBulkImportView.as_view(), name='rir_import'),
     url(r'^rirs/import/$', views.RIRBulkImportView.as_view(), name='rir_import'),
     url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
     url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
     url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
     url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
+    url(r'^vrfs/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}),
 
 
     # Aggregates
     # Aggregates
     url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
     url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
@@ -34,6 +38,7 @@ urlpatterns = [
     url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
     url(r'^aggregates/(?P<pk>\d+)/$', views.AggregateView.as_view(), name='aggregate'),
     url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
     url(r'^aggregates/(?P<pk>\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'),
     url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
     url(r'^aggregates/(?P<pk>\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
+    url(r'^aggregates/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
 
 
     # Roles
     # Roles
     url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
     url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
@@ -41,6 +46,7 @@ urlpatterns = [
     url(r'^roles/import/$', views.RoleBulkImportView.as_view(), name='role_import'),
     url(r'^roles/import/$', views.RoleBulkImportView.as_view(), name='role_import'),
     url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
     url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
     url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
     url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
+    url(r'^roles/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}),
 
 
     # Prefixes
     # Prefixes
     url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
     url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
@@ -51,6 +57,7 @@ urlpatterns = [
     url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
     url(r'^prefixes/(?P<pk>\d+)/$', views.PrefixView.as_view(), name='prefix'),
     url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
     url(r'^prefixes/(?P<pk>\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'),
     url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
     url(r'^prefixes/(?P<pk>\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'),
+    url(r'^prefixes/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}),
     url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
     url(r'^prefixes/(?P<pk>\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'),
     url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
     url(r'^prefixes/(?P<pk>\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'),
 
 
@@ -61,6 +68,7 @@ urlpatterns = [
     url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
     url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
     url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
     url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
     url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
     url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
+    url(r'^ip-addresses/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}),
     url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
     url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
     url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
     url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
     url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
     url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
@@ -72,6 +80,7 @@ urlpatterns = [
     url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
     url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'),
     url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
     url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
     url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
     url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
+    url(r'^vlan-groups/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}),
 
 
     # VLANs
     # VLANs
     url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
     url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
@@ -83,9 +92,11 @@ urlpatterns = [
     url(r'^vlans/(?P<pk>\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'),
     url(r'^vlans/(?P<pk>\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'),
     url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
     url(r'^vlans/(?P<pk>\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'),
     url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
     url(r'^vlans/(?P<pk>\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'),
+    url(r'^vlans/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
 
 
     # Services
     # Services
     url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
     url(r'^services/(?P<pk>\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'),
     url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
     url(r'^services/(?P<pk>\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'),
+    url(r'^services/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}),
 
 
 ]
 ]

+ 4 - 0
netbox/secrets/urls.py

@@ -2,7 +2,9 @@ from __future__ import unicode_literals
 
 
 from django.conf.urls import url
 from django.conf.urls import url
 
 
+from extras.views import ChangeLogView
 from . import views
 from . import views
+from .models import Secret, SecretRole
 
 
 app_name = 'secrets'
 app_name = 'secrets'
 urlpatterns = [
 urlpatterns = [
@@ -13,6 +15,7 @@ urlpatterns = [
     url(r'^secret-roles/import/$', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
     url(r'^secret-roles/import/$', views.SecretRoleBulkImportView.as_view(), name='secretrole_import'),
     url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
     url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
     url(r'^secret-roles/(?P<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
     url(r'^secret-roles/(?P<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),
+    url(r'^secret-roles/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}),
 
 
     # Secrets
     # Secrets
     url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
     url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'),
@@ -22,5 +25,6 @@ urlpatterns = [
     url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
     url(r'^secrets/(?P<pk>\d+)/$', views.SecretView.as_view(), name='secret'),
     url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
     url(r'^secrets/(?P<pk>\d+)/edit/$', views.secret_edit, name='secret_edit'),
     url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
     url(r'^secrets/(?P<pk>\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'),
+    url(r'^secrets/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}),
 
 
 ]
 ]

+ 49 - 36
netbox/templates/circuits/circuit.html

@@ -1,44 +1,57 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
-            <li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
-            <li>{{ circuit.cid }}</li>
-        </ol>
+{% block title %}{{ circuit }}{% endblock %}
+
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'circuits:circuit_list' %}">Circuits</a></li>
+                <li><a href="{% url 'circuits:circuit_list' %}?provider={{ circuit.provider.slug }}">{{ circuit.provider }}</a></li>
+                <li>{{ circuit.cid }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'circuits:circuit_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'circuits:circuit_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if perms.circuits.change_circuit %}
+            <a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this circuit
+            </a>
+        {% endif %}
+        {% if perms.circuits.delete_circuit %}
+            <a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this circuit
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.circuits.change_circuit %}
-		<a href="{% url 'circuits:circuit_edit' pk=circuit.pk %}" class="btn btn-warning">
-			<span class="fa fa-pencil" aria-hidden="true"></span>
-			Edit this circuit
-		</a>
-    {% endif %}
-    {% if perms.circuits.delete_circuit %}
-		<a href="{% url 'circuits:circuit_delete' pk=circuit.pk %}" class="btn btn-danger">
-			<span class="fa fa-trash" aria-hidden="true"></span>
-			Delete this circuit
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=circuit %}
+    <h1>{{ circuit }}</h1>
+    {% include 'inc/created_updated.html' with obj=circuit %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ circuit.get_absolute_url }}">Circuit</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'circuits:circuit_changelog' pk=circuit.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 54 - 41
netbox/templates/circuits/provider.html

@@ -2,49 +2,62 @@
 {% load static from staticfiles %}
 {% load static from staticfiles %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
-            <li>{{ provider }}</li>
-        </ol>
+{% block title %}{{ provider }}{% endblock %}
+
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'circuits:provider_list' %}">Providers</a></li>
+                <li>{{ provider }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'circuits:provider_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'circuits:provider_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if show_graphs %}
+            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
+                <i class="fa fa-signal" aria-hidden="true"></i>
+                Graphs
+            </button>
+        {% endif %}
+        {% if perms.circuits.change_provider %}
+            <a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this provider
+            </a>
+        {% endif %}
+        {% if perms.circuits.delete_provider %}
+            <a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this provider
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if show_graphs %}
-        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ provider.name }}" data-url="{% url 'circuits-api:provider-graphs' pk=provider.pk %}" title="Show graphs">
-            <i class="fa fa-signal" aria-hidden="true"></i>
-            Graphs
-        </button>
-    {% endif %}
-    {% if perms.circuits.change_provider %}
-		<a href="{% url 'circuits:provider_edit' slug=provider.slug %}" class="btn btn-warning">
-			<span class="fa fa-pencil" aria-hidden="true"></span>
-			Edit this provider
-		</a>
-    {% endif %}
-    {% if perms.circuits.delete_provider %}
-		<a href="{% url 'circuits:provider_delete' slug=provider.slug %}" class="btn btn-danger">
-			<span class="fa fa-trash" aria-hidden="true"></span>
-			Delete this provider
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ provider }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=provider %}
+    <h1>{{ provider }}</h1>
+    {% include 'inc/created_updated.html' with obj=provider %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ provider.get_absolute_url }}">Provider</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'circuits:provider_changelog' slug=provider.slug %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-4">
 	<div class="col-md-4">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 8 - 3
netbox/templates/dcim/device.html

@@ -2,6 +2,8 @@
 {% load static from staticfiles %}
 {% load static from staticfiles %}
 {% load helpers %}
 {% load helpers %}
 
 
+{% block title %}{{ device }}{% endblock %}
+
 {% block header %}
 {% block header %}
     <div class="row">
     <div class="row">
         <div class="col-sm-8 col-md-9">
         <div class="col-sm-8 col-md-9">
@@ -45,11 +47,11 @@
             </a>
             </a>
         {% endif %}
         {% endif %}
     </div>
     </div>
-    <h1>{% block title %}{{ device }}{% endblock %}</h1>
+    <h1>{{ device }}</h1>
     {% include 'inc/created_updated.html' with obj=device %}
     {% include 'inc/created_updated.html' with obj=device %}
-    <ul class="nav nav-tabs" style="margin-bottom: 20px">
+    <ul class="nav nav-tabs">
         <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
         <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
-            <a href="{% url 'dcim:device' pk=device.pk %}">Info</a>
+            <a href="{% url 'dcim:device' pk=device.pk %}">Device</a>
         </li>
         </li>
         <li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
         <li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
             <a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
             <a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a>
@@ -67,6 +69,9 @@
                 {% include 'dcim/inc/device_napalm_tabs.html' %}
                 {% include 'dcim/inc/device_napalm_tabs.html' %}
             {% endif %}
             {% endif %}
         {% endif %}
         {% endif %}
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'dcim:device_changelog' pk=device.pk %}">Changelog</a>
+        </li>
     </ul>
     </ul>
 {% endblock %}
 {% endblock %}
 
 

+ 40 - 27
netbox/templates/dcim/devicetype.html

@@ -1,34 +1,47 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-md-12">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'dcim:devicetype_list' %}">Device Types</a></li>
-            <li><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></li>
-            <li>{{ devicetype.model }}</li>
-        </ol>
-    </div>
-</div>
-{% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
-    <div class="pull-right">
-      {% if perms.dcim.change_devicetype %}
-            <a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
-              <span class="fa fa-pencil" aria-hidden="true"></span>
-              Edit this device type
-            </a>
-      {% endif %}
-      {% if perms.dcim.delete_devicetype %}
-          <a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
-          	<span class="fa fa-trash" aria-hidden="true"></span>
-          	Delete this device type
-          </a>
-      {% endif %}
+{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
+
+{% block header %}
+    <div class="row">
+        <div class="col-md-12">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'dcim:devicetype_list' %}">Device Types</a></li>
+                <li><a href="{% url 'dcim:devicetype_list' %}?manufacturer={{ devicetype.manufacturer.slug }}">{{ devicetype.manufacturer }}</a></li>
+                <li>{{ devicetype.model }}</li>
+            </ol>
+        </div>
     </div>
     </div>
-{% endif %}
-<h1>{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=devicetype %}
+    {% if perms.dcim.change_devicetype or perms.dcim.delete_devicetype %}
+        <div class="pull-right">
+          {% if perms.dcim.change_devicetype %}
+                <a href="{% url 'dcim:devicetype_edit' pk=devicetype.pk %}" class="btn btn-warning">
+                  <span class="fa fa-pencil" aria-hidden="true"></span>
+                  Edit this device type
+                </a>
+          {% endif %}
+          {% if perms.dcim.delete_devicetype %}
+              <a href="{% url 'dcim:devicetype_delete' pk=devicetype.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this device type
+              </a>
+          {% endif %}
+        </div>
+    {% endif %}
+    <h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
+    {% include 'inc/created_updated.html' with obj=devicetype %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ devicetype.get_absolute_url }}">Device Type</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'dcim:devicetype_changelog' pk=devicetype.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
     <div class="col-md-5">
     <div class="col-md-5">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 51 - 40
netbox/templates/dcim/rack.html

@@ -1,48 +1,59 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
-            <li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
-            <li>{{ rack }}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
+                <li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
+                <li>{{ rack }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'dcim:rack_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search racks" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'dcim:rack_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search racks" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
+            <span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack
+        </a>
+        <a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
+            <span class="fa fa-chevron-right" aria-hidden="true"></span> Next Rack
+        </a>
+        {% if perms.dcim.change_rack %}
+            <a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span> Edit this rack
+            </a>
+        {% endif %}
+        {% if perms.dcim.delete_rack %}
+            <a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span> Delete this rack
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
-        <span class="fa fa-chevron-left" aria-hidden="true"></span> Previous Rack
-    </a>
-    <a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-primary">
-        <span class="fa fa-chevron-right" aria-hidden="true"></span> Next Rack
-    </a>
-    {% if perms.dcim.change_rack %}
-		<a href="{% url 'dcim:rack_edit' pk=rack.pk %}" class="btn btn-warning">
-			<span class="fa fa-pencil" aria-hidden="true"></span> Edit this rack
-		</a>
-    {% endif %}
-    {% if perms.dcim.delete_rack %}
-		<a href="{% url 'dcim:rack_delete' pk=rack.pk %}" class="btn btn-danger">
-			<span class="fa fa-trash" aria-hidden="true"></span> Delete this rack
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=rack %}
+    <h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=rack %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ rack.get_absolute_url }}">Rack</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'dcim:rack_changelog' pk=rack.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 58 - 46
netbox/templates/dcim/site.html

@@ -3,54 +3,66 @@
 {% load tz %}
 {% load tz %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            {% if site.region %}
-                {% for region in site.region.get_ancestors %}
-                    <li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
-                {% endfor %}
-                <li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
-            {% endif %}
-            <li>{{ site }}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
+                {% if site.region %}
+                    {% for region in site.region.get_ancestors %}
+                        <li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
+                    {% endfor %}
+                    <li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
+                {% endif %}
+                <li>{{ site }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'dcim:site_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search sites" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'dcim:site_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search sites" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if show_graphs %}
+            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs">
+                <i class="fa fa-signal" aria-hidden="true"></i>
+                Graphs
+            </button>
+        {% endif %}
+        {% if perms.dcim.change_site %}
+            <a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this site
+            </a>
+        {% endif %}
+        {% if perms.dcim.delete_site %}
+            <a href="{% url 'dcim:site_delete' slug=site.slug %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this site
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if show_graphs %}
-        <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ site.name }}" data-url="{% url 'dcim-api:site-graphs' pk=site.pk %}" title="Show graphs">
-            <i class="fa fa-signal" aria-hidden="true"></i>
-            Graphs
-        </button>
-    {% endif %}
-    {% if perms.dcim.change_site %}
-		<a href="{% url 'dcim:site_edit' slug=site.slug %}" class="btn btn-warning">
-			<span class="fa fa-pencil" aria-hidden="true"></span>
-			Edit this site
-		</a>
-    {% endif %}
-    {% if perms.dcim.delete_site %}
-		<a href="{% url 'dcim:site_delete' slug=site.slug %}" class="btn btn-danger">
-			<span class="fa fa-trash" aria-hidden="true"></span>
-			Delete this site
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ site }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=site %}
+    <h1>{% block title %}{{ site }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=site %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ site.get_absolute_url }}">Site</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'dcim:site_changelog' slug=site.slug %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-7">
 	<div class="col-md-7">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 36 - 0
netbox/templates/extras/changelog.html

@@ -0,0 +1,36 @@
+{% extends base_template %}
+
+{% block title %}{% if obj %}{{ obj }}{% else %}{{ block.super }}{% endif %} - Changelog{% endblock %}
+
+{% block content %}
+    {% if obj %}<h1>{{ obj }}</h1>{% endif %}
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Time</th>
+                <th>User</th>
+                <th>Action</th>
+                <th></th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for change in changes %}
+                <tr>
+                    <td>{{ change.time }}</td>
+                    <td>{{ change.user }}</td>
+                    <td>{{ change.get_action_display }}</td>
+                    <td>
+                        <button class="btn btn-xs btn-primary" type="button" data-toggle="collapse" data-target="#change{{ change.pk }}" aria-expanded="false" aria-controls="collapseExample">
+                            <i class="fa fa-search"></i>
+                        </button>
+                    </td>
+                </tr>
+                <tr class="collapse" id="change{{ change.pk }}">
+                    <td colspan="4">
+                        <pre class="well">{{ change.object_data_pretty }}</pre>
+                    </td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{% endblock %}

+ 47 - 36
netbox/templates/ipam/aggregate.html

@@ -1,44 +1,55 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
-            <li><a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a></li>
-            <li>{{ aggregate }}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'ipam:aggregate_list' %}">Aggregates</a></li>
+                <li><a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a></li>
+                <li>{{ aggregate }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'ipam:aggregate_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search aggregates" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'ipam:aggregate_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search aggregates" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if perms.ipam.change_aggregate %}
+            <a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this aggregate
+            </a>
+        {% endif %}
+        {% if perms.ipam.delete_aggregate %}
+            <a href="{% url 'ipam:aggregate_delete' pk=aggregate.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this aggregate
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.ipam.change_aggregate %}
-        <a href="{% url 'ipam:aggregate_edit' pk=aggregate.pk %}" class="btn btn-warning">
-            <span class="fa fa-pencil" aria-hidden="true"></span>
-            Edit this aggregate
-        </a>
-    {% endif %}
-    {% if perms.ipam.delete_aggregate %}
-        <a href="{% url 'ipam:aggregate_delete' pk=aggregate.pk %}" class="btn btn-danger">
-            <span class="fa fa-trash" aria-hidden="true"></span>
-            Delete this aggregate
-        </a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=aggregate %}
+    <h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=aggregate %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ aggregate.get_absolute_url }}">Aggregate</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:aggregate_changelog' pk=aggregate.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 49 - 38
netbox/templates/ipam/ipaddress.html

@@ -1,46 +1,57 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
-            {% if ipaddress.vrf %}
-                <li><a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a></li>
-            {% endif %}
-            <li>{{ ipaddress }}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'ipam:ipaddress_list' %}">IP Addresses</a></li>
+                {% if ipaddress.vrf %}
+                    <li><a href="{% url 'ipam:vrf' pk=ipaddress.vrf.pk %}">{{ ipaddress.vrf }}</a></li>
+                {% endif %}
+                <li>{{ ipaddress }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'ipam:ipaddress_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search IPs" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'ipam:ipaddress_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search IPs" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if perms.ipam.change_ipaddress %}
+            <a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this IP
+            </a>
+        {% endif %}
+        {% if perms.ipam.delete_ipaddress %}
+            <a href="{% url 'ipam:ipaddress_delete' pk=ipaddress.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this IP
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.ipam.change_ipaddress %}
-        <a href="{% url 'ipam:ipaddress_edit' pk=ipaddress.pk %}" class="btn btn-warning">
-            <span class="fa fa-pencil" aria-hidden="true"></span>
-            Edit this IP
-        </a>
-    {% endif %}
-    {% if perms.ipam.delete_ipaddress %}
-        <a href="{% url 'ipam:ipaddress_delete' pk=ipaddress.pk %}" class="btn btn-danger">
-            <span class="fa fa-trash" aria-hidden="true"></span>
-            Delete this IP
-        </a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=ipaddress %}
+    <h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=ipaddress %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ ipaddress.get_absolute_url }}">IP Address</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:ipaddress_changelog' pk=ipaddress.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-4">
 	<div class="col-md-4">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 12 - 3
netbox/templates/ipam/prefix.html

@@ -53,9 +53,18 @@
     <h1>{% block title %}{{ prefix }}{% endblock %}</h1>
     <h1>{% block title %}{{ prefix }}{% endblock %}</h1>
     {% include 'inc/created_updated.html' with obj=prefix %}
     {% include 'inc/created_updated.html' with obj=prefix %}
     <ul class="nav nav-tabs" style="margin-bottom: 20px">
     <ul class="nav nav-tabs" style="margin-bottom: 20px">
-        <li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'ipam:prefix' pk=prefix.pk %}">Prefix</a></li>
-        <li role="presentation"{% if active_tab == 'prefixes' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_prefixes' pk=prefix.pk %}">Child Prefixes <span class="badge">{{ prefix.get_child_prefixes.count }}</span></a></li>
-        <li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a></li>
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{% url 'ipam:prefix' pk=prefix.pk %}">Prefix</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'prefixes' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:prefix_prefixes' pk=prefix.pk %}">Child Prefixes <span class="badge">{{ prefix.get_child_prefixes.count }}</span></a>
+        </li>
+        <li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">IP Addresses <span class="badge">{{ prefix.get_child_ips.count }}</span></a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:prefix_changelog' pk=prefix.pk %}">Changelog</a>
+        </li>
     </ul>
     </ul>
 {% endblock %}
 {% endblock %}
 
 

+ 9 - 2
netbox/templates/ipam/vlan.html

@@ -45,8 +45,15 @@
     <h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
     <h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
     {% include 'inc/created_updated.html' with obj=vlan %}
     {% include 'inc/created_updated.html' with obj=vlan %}
     <ul class="nav nav-tabs" style="margin-bottom: 20px">
     <ul class="nav nav-tabs" style="margin-bottom: 20px">
-        <li role="presentation"{% if not active_tab %} class="active"{% endif %}><a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a></li>
-        <li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}><a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a></li>
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:vlan_changelog' pk=vlan.pk %}">Changelog</a>
+        </li>
     </ul>
     </ul>
 {% endblock %}
 {% endblock %}
 
 

+ 46 - 35
netbox/templates/ipam/vrf.html

@@ -1,43 +1,54 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
-            <li>{{ vrf }}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
+                <li>{{ vrf }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'ipam:vrf_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search VRFs" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'ipam:vrf_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search VRFs" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if perms.ipam.change_vrf %}
+            <a href="{% url 'ipam:vrf_edit' pk=vrf.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this VRF
+            </a>
+        {% endif %}
+        {% if perms.ipam.delete_vrf %}
+            <a href="{% url 'ipam:vrf_delete' pk=vrf.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this VRF
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.ipam.change_vrf %}
-        <a href="{% url 'ipam:vrf_edit' pk=vrf.pk %}" class="btn btn-warning">
-            <span class="fa fa-pencil" aria-hidden="true"></span>
-            Edit this VRF
-        </a>
-    {% endif %}
-    {% if perms.ipam.delete_vrf %}
-        <a href="{% url 'ipam:vrf_delete' pk=vrf.pk %}" class="btn btn-danger">
-            <span class="fa fa-trash" aria-hidden="true"></span>
-            Delete this VRF
-        </a>
-    {% endif %}
-</div>
-<h1>{% block title %}VRF {{ vrf }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=vrf %}
+    <h1>{% block title %}VRF {{ vrf }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=vrf %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ aggregate.get_absolute_url }}">VRF</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:vrf_changelog' pk=vrf.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 36 - 25
netbox/templates/secrets/secret.html

@@ -3,32 +3,43 @@
 {% load helpers %}
 {% load helpers %}
 {% load secret_helpers %}
 {% load secret_helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-md-12">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
-            <li><a href="{% url 'secrets:secret_list' %}?role={{ secret.role.slug }}">{{ secret.role }}</a></li>
-            <li>{{ secret.device }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-md-12">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
+                <li><a href="{% url 'secrets:secret_list' %}?role={{ secret.role.slug }}">{{ secret.role }}</a></li>
+                <li>{{ secret.device }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
+            </ol>
+        </div>
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.secrets.change_secret %}
-		<a href="{% url 'secrets:secret_edit' pk=secret.pk %}" class="btn btn-warning">
-			<span class="fa fa-pencil" aria-hidden="true"></span>
-			Edit this secret
-		</a>
-    {% endif %}
-    {% if perms.secrets.delete_secret %}
-		<a href="{% url 'secrets:secret_delete' pk=secret.pk %}" class="btn btn-danger">
-			<span class="fa fa-trash" aria-hidden="true"></span>
-			Delete this secret
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ secret }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=secret %}
+    <div class="pull-right">
+        {% if perms.secrets.change_secret %}
+            <a href="{% url 'secrets:secret_edit' pk=secret.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this secret
+            </a>
+        {% endif %}
+        {% if perms.secrets.delete_secret %}
+            <a href="{% url 'secrets:secret_delete' pk=secret.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this secret
+            </a>
+        {% endif %}
+    </div>
+    <h1>{% block title %}{{ secret }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=secret %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ secret.get_absolute_url }}">Secret</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'secrets:secret_changelog' pk=secret.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 49 - 38
netbox/templates/tenancy/tenant.html

@@ -1,46 +1,57 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
-            {% if tenant.group %}
-                <li><a href="{% url 'tenancy:tenant_list' %}?group={{ tenant.group.slug }}">{{ tenant.group }}</a></li>
-            {% endif %}
-            <li>{{ tenant }}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{% url 'tenancy:tenant_list' %}">Tenants</a></li>
+                {% if tenant.group %}
+                    <li><a href="{% url 'tenancy:tenant_list' %}?group={{ tenant.group.slug }}">{{ tenant.group }}</a></li>
+                {% endif %}
+                <li>{{ tenant }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'tenancy:tenant_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Name" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'tenancy:tenant_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Name" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if perms.tenancy.change_tenant %}
+            <a href="{% url 'tenancy:tenant_edit' slug=tenant.slug %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this tenant
+            </a>
+        {% endif %}
+        {% if perms.tenancy.delete_tenant %}
+            <a href="{% url 'tenancy:tenant_delete' slug=tenant.slug %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this tenant
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.tenancy.change_tenant %}
-		<a href="{% url 'tenancy:tenant_edit' slug=tenant.slug %}" class="btn btn-warning">
-			<span class="fa fa-pencil" aria-hidden="true"></span>
-			Edit this tenant
-		</a>
-    {% endif %}
-    {% if perms.tenancy.delete_tenant %}
-		<a href="{% url 'tenancy:tenant_delete' slug=tenant.slug %}" class="btn btn-danger">
-			<span class="fa fa-trash" aria-hidden="true"></span>
-			Delete this tenant
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ tenant }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=tenant %}
+    <h1>{% block title %}{{ tenant }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=tenant %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ tenant.get_absolute_url }}">Tenant</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'tenancy:tenant_changelog' slug=tenant.slug %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-7">
 	<div class="col-md-7">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 49 - 38
netbox/templates/virtualization/cluster.html

@@ -1,46 +1,57 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row" xmlns="http://www.w3.org/1999/html">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            <li><a href="{{ cluster.type.get_absolute_url }}">{{ cluster.type }}</a></li>
-            {% if cluster.group %}
-                <li><a href="{{ cluster.group.get_absolute_url }}">{{ cluster.group }}</a></li>
-            {% endif %}
-            <li>{{ cluster }}</li>
-        </ol>
+{% block header %}
+    <div class="row" xmlns="http://www.w3.org/1999/html">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                <li><a href="{{ cluster.type.get_absolute_url }}">{{ cluster.type }}</a></li>
+                {% if cluster.group %}
+                    <li><a href="{{ cluster.group.get_absolute_url }}">{{ cluster.group }}</a></li>
+                {% endif %}
+                <li>{{ cluster }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'virtualization:cluster_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search clusters" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search" aria-hidden="true"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'virtualization:cluster_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search clusters" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search" aria-hidden="true"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if perms.virtualization.change_cluster %}
+            <a href="{% url 'virtualization:cluster_edit' pk=cluster.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil" aria-hidden="true"></span>
+                Edit this cluster
+            </a>
+        {% endif %}
+        {% if perms.dcim.delete_cluster %}
+            <a href="{% url 'virtualization:cluster_delete' pk=cluster.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash" aria-hidden="true"></span>
+                Delete this cluster
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.virtualization.change_cluster %}
-		<a href="{% url 'virtualization:cluster_edit' pk=cluster.pk %}" class="btn btn-warning">
-			<span class="fa fa-pencil" aria-hidden="true"></span>
-			Edit this cluster
-		</a>
-    {% endif %}
-    {% if perms.dcim.delete_cluster %}
-		<a href="{% url 'virtualization:cluster_delete' pk=cluster.pk %}" class="btn btn-danger">
-			<span class="fa fa-trash" aria-hidden="true"></span>
-			Delete this cluster
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ cluster }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=cluster %}
+    <h1>{% block title %}{{ cluster }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=cluster %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ cluster.get_absolute_url }}">Cluster</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'virtualization:cluster_changelog' pk=cluster.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-5">
 	<div class="col-md-5">
         <div class="panel panel-default">
         <div class="panel panel-default">

+ 91 - 80
netbox/templates/virtualization/virtualmachine.html

@@ -1,45 +1,56 @@
 {% extends '_base.html' %}
 {% extends '_base.html' %}
 {% load helpers %}
 {% load helpers %}
 
 
-{% block content %}
-<div class="row">
-    <div class="col-sm-8 col-md-9">
-        <ol class="breadcrumb">
-            {% if vm.cluster %}
-                <li><a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a></li>
-            {% endif %}
-            <li>{{ vm }}</li>
-        </ol>
+{% block header %}
+    <div class="row">
+        <div class="col-sm-8 col-md-9">
+            <ol class="breadcrumb">
+                {% if virtualmachine.cluster %}
+                    <li><a href="{{ virtualmachine.cluster.get_absolute_url }}">{{ virtualmachine.cluster }}</a></li>
+                {% endif %}
+                <li>{{ virtualmachine }}</li>
+            </ol>
+        </div>
+        <div class="col-sm-4 col-md-3">
+            <form action="{% url 'virtualization:virtualmachine_list' %}" method="get">
+                <div class="input-group">
+                    <input type="text" name="q" class="form-control" placeholder="Search virtual machines" />
+                    <span class="input-group-btn">
+                        <button type="submit" class="btn btn-primary">
+                            <span class="fa fa-search"></span>
+                        </button>
+                    </span>
+                </div>
+            </form>
+        </div>
     </div>
     </div>
-    <div class="col-sm-4 col-md-3">
-        <form action="{% url 'virtualization:virtualmachine_list' %}" method="get">
-            <div class="input-group">
-                <input type="text" name="q" class="form-control" placeholder="Search virtual machines" />
-                <span class="input-group-btn">
-                    <button type="submit" class="btn btn-primary">
-                        <span class="fa fa-search"></span>
-                    </button>
-                </span>
-            </div>
-        </form>
+    <div class="pull-right">
+        {% if perms.virtualization.change_virtualmachine %}
+            <a href="{% url 'virtualization:virtualmachine_edit' pk=virtualmachine.pk %}" class="btn btn-warning">
+                <span class="fa fa-pencil"></span>
+                Edit this VM
+            </a>
+        {% endif %}
+        {% if perms.virtualization.delete_virtualmachine %}
+            <a href="{% url 'virtualization:virtualmachine_delete' pk=virtualmachine.pk %}" class="btn btn-danger">
+                <span class="fa fa-trash"></span>
+                Delete this VM
+            </a>
+        {% endif %}
     </div>
     </div>
-</div>
-<div class="pull-right">
-    {% if perms.virtualization.change_virtualmachine %}
-		<a href="{% url 'virtualization:virtualmachine_edit' pk=vm.pk %}" class="btn btn-warning">
-			<span class="fa fa-pencil"></span>
-			Edit this VM
-		</a>
-    {% endif %}
-    {% if perms.virtualization.delete_virtualmachine %}
-		<a href="{% url 'virtualization:virtualmachine_delete' pk=vm.pk %}" class="btn btn-danger">
-			<span class="fa fa-trash"></span>
-			Delete this VM
-		</a>
-    {% endif %}
-</div>
-<h1>{% block title %}{{ vm }}{% endblock %}</h1>
-{% include 'inc/created_updated.html' with obj=vm %}
+    <h1>{% block title %}{{ virtualmachine }}{% endblock %}</h1>
+    {% include 'inc/created_updated.html' with obj=virtualmachine %}
+    <ul class="nav nav-tabs">
+        <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
+            <a href="{{ virtualmachine.get_absolute_url }}">Virtual Machine</a>
+        </li>
+        <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
+            <a href="{% url 'virtualization:virtualmachine_changelog' pk=virtualmachine.pk %}">Changelog</a>
+        </li>
+    </ul>
+{% endblock %}
+
+{% block content %}
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">
@@ -49,19 +60,19 @@
             <table class="table table-hover panel-body attr-table">
             <table class="table table-hover panel-body attr-table">
                 <tr>
                 <tr>
                     <td>Name</td>
                     <td>Name</td>
-                    <td>{{ vm }}</td>
+                    <td>{{ virtualmachine }}</td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Status</td>
                     <td>Status</td>
                     <td>
                     <td>
-                        <span class="label label-{{ vm.get_status_class }}">{{ vm.get_status_display }}</span>
+                        <span class="label label-{{ virtualmachine.get_status_class }}">{{ virtualmachine.get_status_display }}</span>
                     </td>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Role</td>
                     <td>Role</td>
                     <td>
                     <td>
-                        {% if vm.role %}
-                            <a href="{% url 'virtualization:virtualmachine_list' %}?role={{ vm.role.slug }}">{{ vm.role }}</a>
+                        {% if virtualmachine.role %}
+                            <a href="{% url 'virtualization:virtualmachine_list' %}?role={{ virtualmachine.role.slug }}">{{ virtualmachine.role }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -70,8 +81,8 @@
                 <tr>
                 <tr>
                     <td>Platform</td>
                     <td>Platform</td>
                     <td>
                     <td>
-                        {% if vm.platform %}
-                            <a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ vm.platform.slug }}">{{ vm.platform }}</a>
+                        {% if virtualmachine.platform %}
+                            <a href="{% url 'virtualization:virtualmachine_list' %}?platform={{ virtualmachine.platform.slug }}">{{ virtualmachine.platform }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -80,12 +91,12 @@
                 <tr>
                 <tr>
                     <td>Tenant</td>
                     <td>Tenant</td>
                     <td>
                     <td>
-                        {% if vm.tenant %}
-                            {% if vm.tenant.group %}
-                                <a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group }}</a>
+                        {% if virtualmachine.tenant %}
+                            {% if virtualmachine.tenant.group %}
+                                <a href="{{ virtualmachine.tenant.group.get_absolute_url }}">{{ virtualmachine.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
-                            <a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
+                            <a href="{{ virtualmachine.tenant.get_absolute_url }}">{{ virtualmachine.tenant }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -94,12 +105,12 @@
                 <tr>
                 <tr>
                     <td>Primary IPv4</td>
                     <td>Primary IPv4</td>
                     <td>
                     <td>
-                        {% if vm.primary_ip4 %}
-                            <a href="{% url 'ipam:ipaddress' pk=vm.primary_ip4.pk %}">{{ vm.primary_ip4.address.ip }}</a>
-                            {% if vm.primary_ip4.nat_inside %}
-                                <span>(NAT for {{ vm.primary_ip4.nat_inside.address.ip }})</span>
-                            {% elif vm.primary_ip4.nat_outside %}
-                                <span>(NAT: {{ vm.primary_ip4.nat_outside.address.ip }})</span>
+                        {% if virtualmachine.primary_ip4 %}
+                            <a href="{% url 'ipam:ipaddress' pk=virtualmachine.primary_ip4.pk %}">{{ virtualmachine.primary_ip4.address.ip }}</a>
+                            {% if virtualmachine.primary_ip4.nat_inside %}
+                                <span>(NAT for {{ virtualmachine.primary_ip4.nat_inside.address.ip }})</span>
+                            {% elif virtualmachine.primary_ip4.nat_outside %}
+                                <span>(NAT: {{ virtualmachine.primary_ip4.nat_outside.address.ip }})</span>
                             {% endif %}
                             {% endif %}
                         {% else %}
                         {% else %}
                             <span class="text-muted">N/A</span>
                             <span class="text-muted">N/A</span>
@@ -109,12 +120,12 @@
                 <tr>
                 <tr>
                     <td>Primary IPv6</td>
                     <td>Primary IPv6</td>
                     <td>
                     <td>
-                        {% if vm.primary_ip6 %}
-                            <a href="{% url 'ipam:ipaddress' pk=vm.primary_ip6.pk %}">{{ vm.primary_ip6.address.ip }}</a>
-                            {% if vm.primary_ip6.nat_inside %}
-                                <span>(NAT for {{ vm.primary_ip6.nat_inside.address.ip }})</span>
-                            {% elif vm.primary_ip6.nat_outside %}
-                                <span>(NAT: {{ vm.primary_ip6.nat_outside.address.ip }})</span>
+                        {% if virtualmachine.primary_ip6 %}
+                            <a href="{% url 'ipam:ipaddress' pk=virtualmachine.primary_ip6.pk %}">{{ virtualmachine.primary_ip6.address.ip }}</a>
+                            {% if virtualmachine.primary_ip6.nat_inside %}
+                                <span>(NAT for {{ virtualmachine.primary_ip6.nat_inside.address.ip }})</span>
+                            {% elif virtualmachine.primary_ip6.nat_outside %}
+                                <span>(NAT: {{ virtualmachine.primary_ip6.nat_outside.address.ip }})</span>
                             {% endif %}
                             {% endif %}
                         {% else %}
                         {% else %}
                             <span class="text-muted">N/A</span>
                             <span class="text-muted">N/A</span>
@@ -124,7 +135,7 @@
                 <tr>
                 <tr>
                     <td>Tags</td>
                     <td>Tags</td>
                     <td>
                     <td>
-                        {% for tag in vm.tags.all %}
+                        {% for tag in virtualmachine.tags.all %}
                             {% tag 'virtualization:virtualmachine_list' tag %}
                             {% tag 'virtualization:virtualmachine_list' tag %}
                         {% empty %}
                         {% empty %}
                             <span class="text-muted">N/A</span>
                             <span class="text-muted">N/A</span>
@@ -133,14 +144,14 @@
                 </tr>
                 </tr>
             </table>
             </table>
         </div>
         </div>
-        {% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
+        {% include 'inc/custom_fields_panel.html' with custom_fields=virtualmachine.get_custom_fields %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Comments</strong>
                 <strong>Comments</strong>
             </div>
             </div>
             <div class="panel-body">
             <div class="panel-body">
-                {% if vm.comments %}
-                    {{ vm.comments|gfm }}
+                {% if virtualmachine.comments %}
+                    {{ virtualmachine.comments|gfm }}
                 {% else %}
                 {% else %}
                     <span class="text-muted">None</span>
                     <span class="text-muted">None</span>
                 {% endif %}
                 {% endif %}
@@ -156,16 +167,16 @@
                 <tr>
                 <tr>
                     <td>Cluster</td>
                     <td>Cluster</td>
                     <td>
                     <td>
-                        {% if vm.cluster.group %}
-                            <a href="{{ vm.cluster.group.get_absolute_url }}">{{ vm.cluster.group }}</a>
+                        {% if virtualmachine.cluster.group %}
+                            <a href="{{ virtualmachine.cluster.group.get_absolute_url }}">{{ virtualmachine.cluster.group }}</a>
                             <i class="fa fa-angle-right"></i>
                             <i class="fa fa-angle-right"></i>
                         {% endif %}
                         {% endif %}
-                        <a href="{{ vm.cluster.get_absolute_url }}">{{ vm.cluster }}</a>
+                        <a href="{{ virtualmachine.cluster.get_absolute_url }}">{{ virtualmachine.cluster }}</a>
                     </td>
                     </td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Cluster Type</td>
                     <td>Cluster Type</td>
-                    <td>{{ vm.cluster.type }}</td>
+                    <td>{{ virtualmachine.cluster.type }}</td>
                 </tr>
                 </tr>
             </table>
             </table>
         </div>
         </div>
@@ -177,8 +188,8 @@
                 <tr>
                 <tr>
                     <td><i class="fa fa-tachometer"></i> Virtual CPUs</td>
                     <td><i class="fa fa-tachometer"></i> Virtual CPUs</td>
                     <td>
                     <td>
-                        {% if vm.vcpus %}
-                            {{ vm.vcpus }}
+                        {% if virtualmachine.vcpus %}
+                            {{ virtualmachine.vcpus }}
                         {% else %}
                         {% else %}
                             <span class="text-muted">N/A</span>
                             <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
@@ -187,8 +198,8 @@
                 <tr>
                 <tr>
                     <td><i class="fa fa-microchip"></i> Memory</td>
                     <td><i class="fa fa-microchip"></i> Memory</td>
                     <td>
                     <td>
-                        {% if vm.memory %}
-                            {{ vm.memory }} MB
+                        {% if virtualmachine.memory %}
+                            {{ virtualmachine.memory }} MB
                         {% else %}
                         {% else %}
                             <span class="text-muted">N/A</span>
                             <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
@@ -197,8 +208,8 @@
                 <tr>
                 <tr>
                     <td><i class="fa fa-hdd-o"></i> Disk Space</td>
                     <td><i class="fa fa-hdd-o"></i> Disk Space</td>
                     <td>
                     <td>
-                        {% if vm.disk %}
-                            {{ vm.disk }} GB
+                        {% if virtualmachine.disk %}
+                            {{ virtualmachine.disk }} GB
                         {% else %}
                         {% else %}
                             <span class="text-muted">N/A</span>
                             <span class="text-muted">N/A</span>
                         {% endif %}
                         {% endif %}
@@ -223,7 +234,7 @@
             {% endif %}
             {% endif %}
             {% if perms.ipam.add_service %}
             {% if perms.ipam.add_service %}
                 <div class="panel-footer text-right">
                 <div class="panel-footer text-right">
-                    <a href="{% url 'virtualization:virtualmachine_service_assign' virtualmachine=vm.pk %}" class="btn btn-xs btn-primary">
+                    <a href="{% url 'virtualization:virtualmachine_service_assign' virtualmachine=virtualmachine.pk %}" class="btn btn-xs btn-primary">
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
                         <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
                     </a>
                     </a>
                 </div>
                 </div>
@@ -236,7 +247,7 @@
         {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
         {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
             <form method="post">
             <form method="post">
             {% csrf_token %}
             {% csrf_token %}
-            <input type="hidden" name="virtual_machine" value="{{ vm.pk }}" />
+            <input type="hidden" name="virtual_machine" value="{{ virtualmachine.pk }}" />
         {% endif %}
         {% endif %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
@@ -264,7 +275,7 @@
                 </thead>
                 </thead>
                 <tbody>
                 <tbody>
                     {% for iface in interfaces %}
                     {% for iface in interfaces %}
-                        {% include 'dcim/inc/interface.html' with device=vm %}
+                        {% include 'dcim/inc/interface.html' with device=virtualmachine %}
                     {% empty %}
                     {% empty %}
                         <tr>
                         <tr>
                             <td colspan="9" class="text-center text-muted">&mdash; No interfaces defined &mdash;</td>
                             <td colspan="9" class="text-center text-muted">&mdash; No interfaces defined &mdash;</td>
@@ -275,21 +286,21 @@
             {% if perms.dcim.add_interface or perms.dcim.delete_interface %}
             {% if perms.dcim.add_interface or perms.dcim.delete_interface %}
                 <div class="panel-footer">
                 <div class="panel-footer">
                     {% if interfaces and perms.dcim.change_interface %}
                     {% if interfaces and perms.dcim.change_interface %}
-                        <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ vm.get_absolute_url }}" class="btn btn-warning btn-xs">
+                        <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-warning btn-xs">
                             <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
                             <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Rename
                         </button>
                         </button>
-                        <button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=vm.pk %}" class="btn btn-warning btn-xs">
+                        <button type="submit" name="_edit" formaction="{% url 'virtualization:interface_bulk_edit' pk=virtualmachine.pk %}" class="btn btn-warning btn-xs">
                             <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
                             <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
                         </button>
                         </button>
                     {% endif %}
                     {% endif %}
                     {% if interfaces and perms.dcim.delete_interface %}
                     {% if interfaces and perms.dcim.delete_interface %}
-                        <button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=vm.pk %}" class="btn btn-danger btn-xs">
+                        <button type="submit" name="_delete" formaction="{% url 'virtualization:interface_bulk_delete' pk=virtualmachine.pk %}" class="btn btn-danger btn-xs">
                             <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
                             <span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
                         </button>
                         </button>
                     {% endif %}
                     {% endif %}
                     {% if perms.dcim.add_interface %}
                     {% if perms.dcim.add_interface %}
                         <div class="pull-right">
                         <div class="pull-right">
-                            <a href="{% url 'virtualization:interface_add' pk=vm.pk %}" class="btn btn-primary btn-xs">
+                            <a href="{% url 'virtualization:interface_add' pk=virtualmachine.pk %}" class="btn btn-primary btn-xs">
                                 <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
                                 <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interfaces
                             </a>
                             </a>
                         </div>
                         </div>

+ 4 - 0
netbox/tenancy/urls.py

@@ -2,7 +2,9 @@ from __future__ import unicode_literals
 
 
 from django.conf.urls import url
 from django.conf.urls import url
 
 
+from extras.views import ChangeLogView
 from . import views
 from . import views
+from .models import Tenant, TenantGroup
 
 
 app_name = 'tenancy'
 app_name = 'tenancy'
 urlpatterns = [
 urlpatterns = [
@@ -13,6 +15,7 @@ urlpatterns = [
     url(r'^tenant-groups/import/$', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
     url(r'^tenant-groups/import/$', views.TenantGroupBulkImportView.as_view(), name='tenantgroup_import'),
     url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
     url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
     url(r'^tenant-groups/(?P<slug>[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
     url(r'^tenant-groups/(?P<slug>[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
+    url(r'^tenant-groups/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='tenantgroup_changelog', kwargs={'model': TenantGroup}),
 
 
     # Tenants
     # Tenants
     url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'),
     url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'),
@@ -23,5 +26,6 @@ urlpatterns = [
     url(r'^tenants/(?P<slug>[\w-]+)/$', views.TenantView.as_view(), name='tenant'),
     url(r'^tenants/(?P<slug>[\w-]+)/$', views.TenantView.as_view(), name='tenant'),
     url(r'^tenants/(?P<slug>[\w-]+)/edit/$', views.TenantEditView.as_view(), name='tenant_edit'),
     url(r'^tenants/(?P<slug>[\w-]+)/edit/$', views.TenantEditView.as_view(), name='tenant_edit'),
     url(r'^tenants/(?P<slug>[\w-]+)/delete/$', views.TenantDeleteView.as_view(), name='tenant_delete'),
     url(r'^tenants/(?P<slug>[\w-]+)/delete/$', views.TenantDeleteView.as_view(), name='tenant_delete'),
+    url(r'^tenants/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='tenant_changelog', kwargs={'model': Tenant}),
 
 
 ]
 ]

+ 6 - 0
netbox/virtualization/urls.py

@@ -2,8 +2,10 @@ from __future__ import unicode_literals
 
 
 from django.conf.urls import url
 from django.conf.urls import url
 
 
+from extras.views import ChangeLogView
 from ipam.views import ServiceCreateView
 from ipam.views import ServiceCreateView
 from . import views
 from . import views
+from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 app_name = 'virtualization'
 app_name = 'virtualization'
 urlpatterns = [
 urlpatterns = [
@@ -14,6 +16,7 @@ urlpatterns = [
     url(r'^cluster-types/import/$', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
     url(r'^cluster-types/import/$', views.ClusterTypeBulkImportView.as_view(), name='clustertype_import'),
     url(r'^cluster-types/delete/$', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
     url(r'^cluster-types/delete/$', views.ClusterTypeBulkDeleteView.as_view(), name='clustertype_bulk_delete'),
     url(r'^cluster-types/(?P<slug>[\w-]+)/edit/$', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
     url(r'^cluster-types/(?P<slug>[\w-]+)/edit/$', views.ClusterTypeEditView.as_view(), name='clustertype_edit'),
+    url(r'^cluster-types/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='clustertype_changelog', kwargs={'model': ClusterType}),
 
 
     # Cluster groups
     # Cluster groups
     url(r'^cluster-groups/$', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
     url(r'^cluster-groups/$', views.ClusterGroupListView.as_view(), name='clustergroup_list'),
@@ -21,6 +24,7 @@ urlpatterns = [
     url(r'^cluster-groups/import/$', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
     url(r'^cluster-groups/import/$', views.ClusterGroupBulkImportView.as_view(), name='clustergroup_import'),
     url(r'^cluster-groups/delete/$', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
     url(r'^cluster-groups/delete/$', views.ClusterGroupBulkDeleteView.as_view(), name='clustergroup_bulk_delete'),
     url(r'^cluster-groups/(?P<slug>[\w-]+)/edit/$', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
     url(r'^cluster-groups/(?P<slug>[\w-]+)/edit/$', views.ClusterGroupEditView.as_view(), name='clustergroup_edit'),
+    url(r'^cluster-groups/(?P<slug>[\w-]+)/changelog/$', ChangeLogView.as_view(), name='clustergroup_changelog', kwargs={'model': ClusterGroup}),
 
 
     # Clusters
     # Clusters
     url(r'^clusters/$', views.ClusterListView.as_view(), name='cluster_list'),
     url(r'^clusters/$', views.ClusterListView.as_view(), name='cluster_list'),
@@ -31,6 +35,7 @@ urlpatterns = [
     url(r'^clusters/(?P<pk>\d+)/$', views.ClusterView.as_view(), name='cluster'),
     url(r'^clusters/(?P<pk>\d+)/$', views.ClusterView.as_view(), name='cluster'),
     url(r'^clusters/(?P<pk>\d+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'),
     url(r'^clusters/(?P<pk>\d+)/edit/$', views.ClusterEditView.as_view(), name='cluster_edit'),
     url(r'^clusters/(?P<pk>\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'),
     url(r'^clusters/(?P<pk>\d+)/delete/$', views.ClusterDeleteView.as_view(), name='cluster_delete'),
+    url(r'^clusters/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='cluster_changelog', kwargs={'model': Cluster}),
     url(r'^clusters/(?P<pk>\d+)/devices/add/$', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
     url(r'^clusters/(?P<pk>\d+)/devices/add/$', views.ClusterAddDevicesView.as_view(), name='cluster_add_devices'),
     url(r'^clusters/(?P<pk>\d+)/devices/remove/$', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
     url(r'^clusters/(?P<pk>\d+)/devices/remove/$', views.ClusterRemoveDevicesView.as_view(), name='cluster_remove_devices'),
 
 
@@ -43,6 +48,7 @@ urlpatterns = [
     url(r'^virtual-machines/(?P<pk>\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'),
     url(r'^virtual-machines/(?P<pk>\d+)/$', views.VirtualMachineView.as_view(), name='virtualmachine'),
     url(r'^virtual-machines/(?P<pk>\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
     url(r'^virtual-machines/(?P<pk>\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'),
     url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
     url(r'^virtual-machines/(?P<pk>\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'),
+    url(r'^virtual-machines/(?P<pk>\d+)/changelog/$', ChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}),
     url(r'^virtual-machines/(?P<virtualmachine>\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'),
     url(r'^virtual-machines/(?P<virtualmachine>\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'),
 
 
     # VM interfaces
     # VM interfaces

+ 4 - 4
netbox/virtualization/views.py

@@ -258,12 +258,12 @@ class VirtualMachineView(View):
 
 
     def get(self, request, pk):
     def get(self, request, pk):
 
 
-        vm = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
-        interfaces = Interface.objects.filter(virtual_machine=vm)
-        services = Service.objects.filter(virtual_machine=vm)
+        virtualmachine = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)
+        interfaces = Interface.objects.filter(virtual_machine=virtualmachine)
+        services = Service.objects.filter(virtual_machine=virtualmachine)
 
 
         return render(request, 'virtualization/virtualmachine.html', {
         return render(request, 'virtualization/virtualmachine.html', {
-            'vm': vm,
+            'virtualmachine': virtualmachine,
             'interfaces': interfaces,
             'interfaces': interfaces,
             'services': services,
             'services': services,
         })
         })