diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 6b5529a42..449da3964 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from extras.views import ChangeLogView +from extras.views import ObjectChangeLogView from . import views from .models import Circuit, CircuitType, Provider @@ -18,7 +18,7 @@ urlpatterns = [ url(r'^providers/(?P[\w-]+)/$', views.ProviderView.as_view(), name='provider'), url(r'^providers/(?P[\w-]+)/edit/$', views.ProviderEditView.as_view(), name='provider_edit'), url(r'^providers/(?P[\w-]+)/delete/$', views.ProviderDeleteView.as_view(), name='provider_delete'), - url(r'^providers/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), + url(r'^providers/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), # Circuit types url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'), @@ -26,7 +26,7 @@ urlpatterns = [ 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/(?P[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), - url(r'^circuit-types/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), + url(r'^circuit-types/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), # Circuits url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'), @@ -37,7 +37,7 @@ urlpatterns = [ url(r'^circuits/(?P\d+)/$', views.CircuitView.as_view(), name='circuit'), url(r'^circuits/(?P\d+)/edit/$', views.CircuitEditView.as_view(), name='circuit_edit'), url(r'^circuits/(?P\d+)/delete/$', views.CircuitDeleteView.as_view(), name='circuit_delete'), - url(r'^circuits/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), + url(r'^circuits/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), url(r'^circuits/(?P\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), # Circuit terminations diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 67e957f9b..de1cbd4cc 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from extras.views import ChangeLogView, ImageAttachmentEditView +from extras.views import ObjectChangeLogView, ImageAttachmentEditView from ipam.views import ServiceCreateView from secrets.views import secret_add from . import views @@ -20,7 +20,7 @@ urlpatterns = [ 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/(?P\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'), - url(r'^regions/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), + url(r'^regions/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='region_changelog', kwargs={'model': Region}), # Sites url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), @@ -30,7 +30,7 @@ urlpatterns = [ url(r'^sites/(?P[\w-]+)/$', views.SiteView.as_view(), name='site'), url(r'^sites/(?P[\w-]+)/edit/$', views.SiteEditView.as_view(), name='site_edit'), url(r'^sites/(?P[\w-]+)/delete/$', views.SiteDeleteView.as_view(), name='site_delete'), - url(r'^sites/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), + url(r'^sites/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), url(r'^sites/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='site_add_image', kwargs={'model': Site}), # Rack groups @@ -39,7 +39,7 @@ urlpatterns = [ 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/(?P\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), - url(r'^rack-groups/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), + url(r'^rack-groups/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackgroup_changelog', kwargs={'model': RackGroup}), # Rack roles url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), @@ -47,7 +47,7 @@ urlpatterns = [ 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/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), - url(r'^rack-roles/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), + url(r'^rack-roles/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackrole_changelog', kwargs={'model': RackRole}), # Rack reservations url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'), @@ -55,7 +55,7 @@ urlpatterns = [ url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'), url(r'^rack-reservations/(?P\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'), url(r'^rack-reservations/(?P\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'), - url(r'^rack-reservations/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), + url(r'^rack-reservations/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rackreservation_changelog', kwargs={'model': RackReservation}), # Racks url(r'^racks/$', views.RackListView.as_view(), name='rack_list'), @@ -67,7 +67,7 @@ urlpatterns = [ url(r'^racks/(?P\d+)/$', views.RackView.as_view(), name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), - url(r'^racks/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), + url(r'^racks/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='rack_changelog', kwargs={'model': Rack}), url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), url(r'^racks/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), @@ -77,7 +77,7 @@ urlpatterns = [ 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/(?P[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), - url(r'^manufacturers/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), + url(r'^manufacturers/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='manufacturer_changelog', kwargs={'model': Manufacturer}), # Device types url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), @@ -88,7 +88,7 @@ urlpatterns = [ url(r'^device-types/(?P\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), url(r'^device-types/(?P\d+)/edit/$', views.DeviceTypeEditView.as_view(), name='devicetype_edit'), url(r'^device-types/(?P\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), - url(r'^device-types/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), + url(r'^device-types/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='devicetype_changelog', kwargs={'model': DeviceType}), # Console port templates url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), @@ -121,7 +121,7 @@ urlpatterns = [ 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/(?P[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), - url(r'^device-roles/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), + url(r'^device-roles/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='devicerole_changelog', kwargs={'model': DeviceRole}), # Platforms url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'), @@ -129,7 +129,7 @@ urlpatterns = [ 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/(?P[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'), - url(r'^platforms/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), + url(r'^platforms/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='platform_changelog', kwargs={'model': Platform}), # Devices url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'), @@ -141,7 +141,7 @@ urlpatterns = [ url(r'^devices/(?P\d+)/$', views.DeviceView.as_view(), name='device'), url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), - url(r'^devices/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), + url(r'^devices/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'), url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), @@ -235,7 +235,7 @@ urlpatterns = [ url(r'^virtual-chassis/add/$', views.VirtualChassisCreateView.as_view(), name='virtualchassis_add'), url(r'^virtual-chassis/(?P\d+)/edit/$', views.VirtualChassisEditView.as_view(), name='virtualchassis_edit'), url(r'^virtual-chassis/(?P\d+)/delete/$', views.VirtualChassisDeleteView.as_view(), name='virtualchassis_delete'), - url(r'^virtual-chassis/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), + url(r'^virtual-chassis/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualchassis_changelog', kwargs={'model': VirtualChassis}), url(r'^virtual-chassis/(?P\d+)/add-member/$', views.VirtualChassisAddMemberView.as_view(), name='virtualchassis_add_member'), url(r'^virtual-chassis-members/(?P\d+)/delete/$', views.VirtualChassisRemoveMemberView.as_view(), name='virtualchassis_remove_member'), diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index 52eef54b7..dbdf26c6f 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -129,6 +129,7 @@ class ObjectChangeFilter(django_filters.FilterSet): method='search', label='Search', ) + time = django_filters.DateTimeFromToRangeFilter() class Meta: model = ObjectChange diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 9088d1b3d..a39814eb6 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -3,12 +3,16 @@ from __future__ import unicode_literals from collections import OrderedDict from django import forms +from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from taggit.models import Tag -from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField, SlugField -from .constants import CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL -from .models import CustomField, CustomFieldValue, ImageAttachment +from utilities.forms import add_blank_choice, BootstrapMixin, BulkEditForm, LaxURLField, SlugField +from .constants import ( + CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, + OBJECTCHANGE_ACTION_CHOICES, +) +from .models import CustomField, CustomFieldValue, ImageAttachment, ObjectChange # @@ -189,3 +193,38 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): class Meta: model = ImageAttachment fields = ['name', 'image'] + + +# +# Change logging +# + +class ObjectChangeFilterForm(BootstrapMixin, CustomFieldFilterForm): + model = ObjectChange + q = forms.CharField( + required=False, + label='Search' + ) + # TODO: Change time_0 and time_1 to time_after and time_before for django-filter==2.0 + time_0 = forms.DateTimeField( + label='After', + required=False, + widget=forms.TextInput( + attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'} + ) + ) + time_1 = forms.DateTimeField( + label='Before', + required=False, + widget=forms.TextInput( + attrs={'placeholder': 'YYYY-MM-DD hh:mm:ss'} + ) + ) + action = forms.ChoiceField( + choices=add_blank_choice(OBJECTCHANGE_ACTION_CHOICES), + required=False + ) + user = forms.ModelChoiceField( + queryset=User.objects.order_by('username'), + required=False + ) diff --git a/netbox/extras/models.py b/netbox/extras/models.py index da7356540..2a225b0fe 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import JSONField +from django.core.urlresolvers import reverse from django.core.validators import ValidationError from django.db import models from django.db.models import Q @@ -704,15 +705,18 @@ class ObjectChange(models.Model): editable=False ) + serializer = 'extras.api.serializers.ObjectChangeSerializer' + csv_headers = ['time', 'user', 'request_id', 'action', 'content_type', 'object_id', 'object_repr', 'object_data'] + class Meta: ordering = ['-time'] def __str__(self): - attribution = 'by {}'.format(self.user_name) if self.user_name else '(no attribution)' - return '{} {} {}'.format( + return '{} {} {} by {}'.format( + self.content_type, self.object_repr, self.get_action_display().lower(), - attribution + self.user_name ) def save(self, *args, **kwargs): @@ -724,6 +728,21 @@ class ObjectChange(models.Model): return super(ObjectChange, self).save(*args, **kwargs) + def get_absolute_url(self): + return reverse('extras:objectchange', args=[self.pk]) + + def to_csv(self): + return ( + self.time, + self.user or self.user_name, + self.request_id, + self.get_action_display(), + self.content_type, + self.object_id, + self.object_repr, + self.object_data, + ) + @property def object_data_pretty(self): return json.dumps(self.object_data, indent=4, sort_keys=True) diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 921b9f273..afc5f2a53 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -4,6 +4,7 @@ import django_tables2 as tables from taggit.models import Tag from utilities.tables import BaseTable, ToggleColumn +from .models import ObjectChange TAG_ACTIONS = """ {% if perms.taggit.change_tag %} @@ -14,6 +15,24 @@ TAG_ACTIONS = """ {% endif %} """ +OBJECTCHANGE_ACTION = """ +{% if record.action == 1 %} + Created +{% elif record.action == 2 %} + Updated +{% elif record.action == 3 %} + Deleted +{% endif %} +""" + +OBJECTCHANGE_OBJECT = """ +{% if record.action != 3 and record.changed_object.get_absolute_url %} + {{ record.object_repr }} +{% else %} + {{ record.object_repr }} +{% endif %} +""" + class TagTable(BaseTable): pk = ToggleColumn() @@ -26,3 +45,21 @@ class TagTable(BaseTable): class Meta(BaseTable.Meta): model = Tag fields = ('pk', 'name', 'items') + + +class ObjectChangeTable(BaseTable): + time = tables.LinkColumn() + action = tables.TemplateColumn( + template_code=OBJECTCHANGE_ACTION + ) + object_repr = tables.TemplateColumn( + template_code=OBJECTCHANGE_OBJECT, + verbose_name='Object' + ) + request_id = tables.Column( + verbose_name='Request ID' + ) + + class Meta(BaseTable.Meta): + model = ObjectChange + fields = ('time', 'user_name', 'action', 'content_type', 'object_repr', 'request_id') diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index d3c200334..d92303264 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -22,4 +22,8 @@ urlpatterns = [ url(r'^reports/(?P[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'), url(r'^reports/(?P[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'), + # Change logging + url(r'^changelog/$', views.ObjectChangeListView.as_view(), name='objectchange_list'), + url(r'^changelog/(?P\d+)/$', views.ObjectChangeView.as_view(), name='objectchange'), + ] diff --git a/netbox/extras/views.py b/netbox/extras/views.py index d3dd2c3d2..393407514 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -13,10 +13,11 @@ from taggit.models import Tag from utilities.forms import ConfirmationForm from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView -from .forms import ImageAttachmentForm, TagForm +from . import filters +from .forms import ObjectChangeFilterForm, ImageAttachmentForm, TagForm from .models import ImageAttachment, ObjectChange, ReportResult, UserAction from .reports import get_report, get_reports -from .tables import TagTable +from .tables import ObjectChangeTable, TagTable # @@ -56,9 +57,36 @@ class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Change logging # -class ChangeLogView(View): +class ObjectChangeListView(ObjectListView): + queryset = ObjectChange.objects.select_related('user', 'content_type') + filter = filters.ObjectChangeFilter + filter_form = ObjectChangeFilterForm + table = ObjectChangeTable + template_name = 'extras/objectchange_list.html' + + +class ObjectChangeView(View): + + def get(self, request, pk): + + objectchange = get_object_or_404(ObjectChange, pk=pk) + + related_changes = ObjectChange.objects.filter(request_id=objectchange.request_id).exclude(pk=objectchange.pk) + related_changes_table = ObjectChangeTable( + data=related_changes[:50], + orderable=False + ) + + return render(request, 'extras/objectchange.html', { + 'objectchange': objectchange, + 'related_changes_table': related_changes_table, + 'related_changes_count': related_changes.count() + }) + + +class ObjectChangeLogView(View): """ - Present a history of changes made to an object. + Present a history of changes made to a particular object. """ def get(self, request, model, **kwargs): @@ -68,7 +96,16 @@ class ChangeLogView(View): # 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) + objectchanges = ObjectChange.objects.select_related( + 'user', 'content_type' + ).filter( + content_type=content_type, + object_id=obj.pk + ) + objectchanges_table = ObjectChangeTable( + data=objectchanges, + orderable=False + ) # Check whether a header template exists for this model base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name) @@ -79,9 +116,9 @@ class ChangeLogView(View): base_template = '_base.html' object_var = 'obj' - return render(request, 'extras/changelog.html', { + return render(request, 'extras/object_changelog.html', { object_var: obj, - 'changes': changes, + 'objectchanges_table': objectchanges_table, 'base_template': base_template, 'active_tab': 'changelog', }) diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 89c30a6db..2313fedb9 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from extras.views import ChangeLogView +from extras.views import ObjectChangeLogView from . import views from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -19,7 +19,7 @@ urlpatterns = [ url(r'^vrfs/(?P\d+)/$', views.VRFView.as_view(), name='vrf'), url(r'^vrfs/(?P\d+)/edit/$', views.VRFEditView.as_view(), name='vrf_edit'), url(r'^vrfs/(?P\d+)/delete/$', views.VRFDeleteView.as_view(), name='vrf_delete'), - url(r'^vrfs/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), + url(r'^vrfs/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vrf_changelog', kwargs={'model': VRF}), # RIRs url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'), @@ -27,7 +27,7 @@ urlpatterns = [ 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/(?P[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), - url(r'^vrfs/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), + url(r'^vrfs/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='rir_changelog', kwargs={'model': RIR}), # Aggregates url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'), @@ -38,7 +38,7 @@ urlpatterns = [ url(r'^aggregates/(?P\d+)/$', views.AggregateView.as_view(), name='aggregate'), url(r'^aggregates/(?P\d+)/edit/$', views.AggregateEditView.as_view(), name='aggregate_edit'), url(r'^aggregates/(?P\d+)/delete/$', views.AggregateDeleteView.as_view(), name='aggregate_delete'), - url(r'^aggregates/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), + url(r'^aggregates/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), # Roles url(r'^roles/$', views.RoleListView.as_view(), name='role_list'), @@ -46,7 +46,7 @@ urlpatterns = [ 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/(?P[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'), - url(r'^roles/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), + url(r'^roles/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='role_changelog', kwargs={'model': Role}), # Prefixes url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'), @@ -57,7 +57,7 @@ urlpatterns = [ url(r'^prefixes/(?P\d+)/$', views.PrefixView.as_view(), name='prefix'), url(r'^prefixes/(?P\d+)/edit/$', views.PrefixEditView.as_view(), name='prefix_edit'), url(r'^prefixes/(?P\d+)/delete/$', views.PrefixDeleteView.as_view(), name='prefix_delete'), - url(r'^prefixes/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), + url(r'^prefixes/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='prefix_changelog', kwargs={'model': Prefix}), url(r'^prefixes/(?P\d+)/prefixes/$', views.PrefixPrefixesView.as_view(), name='prefix_prefixes'), url(r'^prefixes/(?P\d+)/ip-addresses/$', views.PrefixIPAddressesView.as_view(), name='prefix_ipaddresses'), @@ -68,7 +68,7 @@ urlpatterns = [ 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/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), - url(r'^ip-addresses/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), + url(r'^ip-addresses/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='ipaddress_changelog', kwargs={'model': IPAddress}), url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'), url(r'^ip-addresses/(?P\d+)/$', views.IPAddressView.as_view(), name='ipaddress'), url(r'^ip-addresses/(?P\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), @@ -80,7 +80,7 @@ urlpatterns = [ 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/(?P\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), - url(r'^vlan-groups/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), + url(r'^vlan-groups/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlangroup_changelog', kwargs={'model': VLANGroup}), # VLANs url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), @@ -92,11 +92,11 @@ urlpatterns = [ url(r'^vlans/(?P\d+)/members/$', views.VLANMembersView.as_view(), name='vlan_members'), url(r'^vlans/(?P\d+)/edit/$', views.VLANEditView.as_view(), name='vlan_edit'), url(r'^vlans/(?P\d+)/delete/$', views.VLANDeleteView.as_view(), name='vlan_delete'), - url(r'^vlans/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), + url(r'^vlans/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}), # Services url(r'^services/(?P\d+)/edit/$', views.ServiceEditView.as_view(), name='service_edit'), url(r'^services/(?P\d+)/delete/$', views.ServiceDeleteView.as_view(), name='service_delete'), - url(r'^services/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), + url(r'^services/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='service_changelog', kwargs={'model': Service}), ] diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index 9c9324c4a..952725b54 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from extras.views import ChangeLogView +from extras.views import ObjectChangeLogView from . import views from .models import Secret, SecretRole @@ -15,7 +15,7 @@ urlpatterns = [ 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/(?P[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'), - url(r'^secret-roles/(?P[\w-]+)/changelog/$', ChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), + url(r'^secret-roles/(?P[\w-]+)/changelog/$', ObjectChangeLogView.as_view(), name='secretrole_changelog', kwargs={'model': SecretRole}), # Secrets url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'), @@ -25,6 +25,6 @@ urlpatterns = [ url(r'^secrets/(?P\d+)/$', views.SecretView.as_view(), name='secret'), url(r'^secrets/(?P\d+)/edit/$', views.secret_edit, name='secret_edit'), url(r'^secrets/(?P\d+)/delete/$', views.SecretDeleteView.as_view(), name='secret_delete'), - url(r'^secrets/(?P\d+)/changelog/$', ChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), + url(r'^secrets/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='secret_changelog', kwargs={'model': Secret}), ] diff --git a/netbox/templates/extras/changelog.html b/netbox/templates/extras/changelog.html deleted file mode 100644 index 4a0235f62..000000000 --- a/netbox/templates/extras/changelog.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends base_template %} - -{% block title %}{% if obj %}{{ obj }}{% else %}{{ block.super }}{% endif %} - Changelog{% endblock %} - -{% block content %} - {% if obj %}

{{ obj }}

{% endif %} - - - - - - - - - - - {% for change in changes %} - - - - - - - - - - {% endfor %} - -
TimeUserAction
{{ change.time }}{{ change.user }}{{ change.get_action_display }} - -
-
{{ change.object_data_pretty }}
-
-{% endblock %} diff --git a/netbox/templates/extras/object_changelog.html b/netbox/templates/extras/object_changelog.html new file mode 100644 index 000000000..ac79be2a6 --- /dev/null +++ b/netbox/templates/extras/object_changelog.html @@ -0,0 +1,8 @@ +{% extends base_template %} + +{% block title %}{% if obj %}{{ obj }}{% else %}{{ block.super }}{% endif %} - Changelog{% endblock %} + +{% block content %} + {% if obj %}

{{ obj }}

{% endif %} + {% include 'panel_table.html' with table=objectchanges_table %} +{% endblock %} diff --git a/netbox/templates/extras/objectchange.html b/netbox/templates/extras/objectchange.html new file mode 100644 index 000000000..fb63e34c9 --- /dev/null +++ b/netbox/templates/extras/objectchange.html @@ -0,0 +1,101 @@ +{% extends '_base.html' %} +{% load helpers %} + +{% block title %}{{ objectchange }}{% endblock %} + +{% block header %} +
+
+ +
+
+
+
+ + + + +
+
+
+
+{% endblock %} + +{% block content %} +
+
+
+
+ Change +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Time + {{ objectchange.time }} +
User + {{ objectchange.user|default:objectchange.user_name }} +
Action + {{ objectchange.get_action_display }} +
Content Type + {{ objectchange.content_type }} +
Object + {% if objectchange.changed_object.get_absolute_url %} + {{ objectchange.changed_object }} + {% else %} + {{ objectchange.object_repr }} + {% endif %} +
Request ID + {{ objectchange.request_id }} +
+
+
+
+
+
+ Object Data +
+
+
{{ objectchange.object_data_pretty }}
+
+
+
+
+
+
+ {% include 'panel_table.html' with table=related_changes_table heading='Related Changes' %} + {% if related_changes_count > related_changes_table.rows|length %} + + {% endif %} +
+
+{% endblock %} diff --git a/netbox/templates/extras/objectchange_list.html b/netbox/templates/extras/objectchange_list.html new file mode 100644 index 000000000..46ddc1d94 --- /dev/null +++ b/netbox/templates/extras/objectchange_list.html @@ -0,0 +1,17 @@ +{% extends '_base.html' %} +{% load buttons %} + +{% block content %} +
+ {% export_button content_type %} +
+

{% block title %}Changelog{% endblock %}

+
+
+ {% include 'utilities/obj_table.html' %} +
+
+ {% include 'inc/search_panel.html' %} +
+
+{% endblock %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index 2c47ad85b..7011bff2b 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -66,6 +66,9 @@
  • Reports
  • +
  • + Changelog +