mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-07 04:27:27 -06:00
Implemented changelog views
This commit is contained in:
@@ -129,6 +129,7 @@ class ObjectChangeFilter(django_filters.FilterSet):
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
<span class="label label-success">Created</span>
|
||||
{% elif record.action == 2 %}
|
||||
<span class="label label-primary">Updated</span>
|
||||
{% elif record.action == 3 %}
|
||||
<span class="label label-danger">Deleted</span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
OBJECTCHANGE_OBJECT = """
|
||||
{% if record.action != 3 and record.changed_object.get_absolute_url %}
|
||||
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||
{% 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')
|
||||
|
||||
@@ -22,4 +22,8 @@ urlpatterns = [
|
||||
url(r'^reports/(?P<name>[^/]+\.[^/]+)/$', views.ReportView.as_view(), name='report'),
|
||||
url(r'^reports/(?P<name>[^/]+\.[^/]+)/run/$', views.ReportRunView.as_view(), name='report_run'),
|
||||
|
||||
# Change logging
|
||||
url(r'^changelog/$', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
url(r'^changelog/(?P<pk>\d+)/$', views.ObjectChangeView.as_view(), name='objectchange'),
|
||||
|
||||
]
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user