#8334: Move object changelog & journaling to generic views

This commit is contained in:
jeremystretch 2022-02-09 16:24:10 -05:00
parent 7c105019d8
commit d42c59792f
11 changed files with 150 additions and 110 deletions

View File

@ -146,9 +146,9 @@ A `PluginMenuButton` has the following attributes:
!!! note !!! note
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons. Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
## Object Views Reference ## Object Views
Below is the class definition for NetBox's BaseObjectView. The attributes and methods defined here are available on all generic views which handle a single object. Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
::: netbox.views.generic.base.BaseObjectView ::: netbox.views.generic.base.BaseObjectView
rendering: rendering:
@ -177,9 +177,9 @@ Below is the class definition for NetBox's BaseObjectView. The attributes and me
rendering: rendering:
show_source: false show_source: false
## Multi-Object Views Reference ## Multi-Object Views
Below is the class definition for NetBox's BaseMultiObjectView. The attributes and methods defined here are available on all generic views which deal with multiple objects. Below are the class definitions for NetBox's multi-object views. These views handle simultaneous actions for sets objects. The list, import, edit, and delete views each inherit from `BaseMultiObjectView`, which is not intended to be used directly.
::: netbox.views.generic.base.BaseMultiObjectView ::: netbox.views.generic.base.BaseMultiObjectView
rendering: rendering:
@ -212,3 +212,21 @@ Below is the class definition for NetBox's BaseMultiObjectView. The attributes a
- get_form - get_form
rendering: rendering:
show_source: false show_source: false
## Feature Views
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
::: netbox.views.generic.ObjectChangeLogView
selection:
members:
- get_form
rendering:
show_source: false
::: netbox.views.generic.ObjectJournalView
selection:
members:
- get_form
rendering:
show_source: false

View File

@ -1,7 +1,7 @@
from django.urls import path from django.urls import path
from dcim.views import CableCreateView, PathTraceView from dcim.views import CableCreateView, PathTraceView
from extras.views import ObjectChangeLogView, ObjectJournalView from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from . import views from . import views
from .models import * from .models import *

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from extras.views import ObjectChangeLogView, ObjectJournalView from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from . import views from . import views
from .models import * from .models import *

View File

@ -1,6 +1,7 @@
from django.urls import path from django.urls import path
from extras import models, views from extras import models, views
from netbox.views.generic import ObjectChangeLogView
app_name = 'extras' app_name = 'extras'
@ -15,7 +16,7 @@ urlpatterns = [
path('custom-fields/<int:pk>/', views.CustomFieldView.as_view(), name='customfield'), path('custom-fields/<int:pk>/', views.CustomFieldView.as_view(), name='customfield'),
path('custom-fields/<int:pk>/edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'), path('custom-fields/<int:pk>/edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'),
path('custom-fields/<int:pk>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'), path('custom-fields/<int:pk>/delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'),
path('custom-fields/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customfield_changelog', path('custom-fields/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='customfield_changelog',
kwargs={'model': models.CustomField}), kwargs={'model': models.CustomField}),
# Custom links # Custom links
@ -27,7 +28,7 @@ urlpatterns = [
path('custom-links/<int:pk>/', views.CustomLinkView.as_view(), name='customlink'), path('custom-links/<int:pk>/', views.CustomLinkView.as_view(), name='customlink'),
path('custom-links/<int:pk>/edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'), path('custom-links/<int:pk>/edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'),
path('custom-links/<int:pk>/delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'), path('custom-links/<int:pk>/delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'),
path('custom-links/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='customlink_changelog', path('custom-links/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='customlink_changelog',
kwargs={'model': models.CustomLink}), kwargs={'model': models.CustomLink}),
# Export templates # Export templates
@ -39,7 +40,7 @@ urlpatterns = [
path('export-templates/<int:pk>/', views.ExportTemplateView.as_view(), name='exporttemplate'), path('export-templates/<int:pk>/', views.ExportTemplateView.as_view(), name='exporttemplate'),
path('export-templates/<int:pk>/edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'), path('export-templates/<int:pk>/edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'),
path('export-templates/<int:pk>/delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'), path('export-templates/<int:pk>/delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'),
path('export-templates/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog', path('export-templates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='exporttemplate_changelog',
kwargs={'model': models.ExportTemplate}), kwargs={'model': models.ExportTemplate}),
# Webhooks # Webhooks
@ -51,7 +52,7 @@ urlpatterns = [
path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'), path('webhooks/<int:pk>/', views.WebhookView.as_view(), name='webhook'),
path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'), path('webhooks/<int:pk>/edit/', views.WebhookEditView.as_view(), name='webhook_edit'),
path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'), path('webhooks/<int:pk>/delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'),
path('webhooks/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='webhook_changelog', path('webhooks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='webhook_changelog',
kwargs={'model': models.Webhook}), kwargs={'model': models.Webhook}),
# Tags # Tags
@ -63,7 +64,7 @@ urlpatterns = [
path('tags/<int:pk>/', views.TagView.as_view(), name='tag'), path('tags/<int:pk>/', views.TagView.as_view(), name='tag'),
path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'), path('tags/<int:pk>/edit/', views.TagEditView.as_view(), name='tag_edit'),
path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'), path('tags/<int:pk>/delete/', views.TagDeleteView.as_view(), name='tag_delete'),
path('tags/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', path('tags/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='tag_changelog',
kwargs={'model': models.Tag}), kwargs={'model': models.Tag}),
# Config contexts # Config contexts
@ -74,7 +75,7 @@ urlpatterns = [
path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'), path('config-contexts/<int:pk>/', views.ConfigContextView.as_view(), name='configcontext'),
path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), path('config-contexts/<int:pk>/edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'),
path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), path('config-contexts/<int:pk>/delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'),
path('config-contexts/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='configcontext_changelog', path('config-contexts/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='configcontext_changelog',
kwargs={'model': models.ConfigContext}), kwargs={'model': models.ConfigContext}),
# Image attachments # Image attachments
@ -90,7 +91,7 @@ urlpatterns = [
path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'), path('journal-entries/<int:pk>/', views.JournalEntryView.as_view(), name='journalentry'),
path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'), path('journal-entries/<int:pk>/edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'),
path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'), path('journal-entries/<int:pk>/delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'),
path('journal-entries/<int:pk>/changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog', path('journal-entries/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='journalentry_changelog',
kwargs={'model': models.JournalEntry}), kwargs={'model': models.JournalEntry}),
# Change logging # Change logging

View File

@ -422,49 +422,6 @@ class ObjectChangeView(generic.ObjectView):
} }
class ObjectChangeLogView(View):
"""
Present a history of changes made to a particular object.
base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
"""
base_template = None
def get(self, request, model, **kwargs):
# Handle QuerySet restriction of parent object if needed
if hasattr(model.objects, 'restrict'):
obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
else:
obj = get_object_or_404(model, **kwargs)
# Gather all changes for this object (and its related objects)
content_type = ContentType.objects.get_for_model(model)
objectchanges = ObjectChange.objects.restrict(request.user, 'view').prefetch_related(
'user', 'changed_object_type'
).filter(
Q(changed_object_type=content_type, changed_object_id=obj.pk) |
Q(related_object_type=content_type, related_object_id=obj.pk)
)
objectchanges_table = tables.ObjectChangeTable(
data=objectchanges,
orderable=False
)
objectchanges_table.configure(request)
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
# fall back to using base.html.
if self.base_template is None:
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
return render(request, 'extras/object_changelog.html', {
'object': obj,
'table': objectchanges_table,
'base_template': self.base_template,
'active_tab': 'changelog',
})
# #
# Image attachments # Image attachments
# #
@ -547,55 +504,6 @@ class JournalEntryBulkDeleteView(generic.BulkDeleteView):
table = tables.JournalEntryTable table = tables.JournalEntryTable
class ObjectJournalView(View):
"""
Show all journal entries for an object.
base_template: The name of the template to extend. If not provided, "<app>/<model>.html" will be used.
"""
base_template = None
def get(self, request, model, **kwargs):
# Handle QuerySet restriction of parent object if needed
if hasattr(model.objects, 'restrict'):
obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
else:
obj = get_object_or_404(model, **kwargs)
# Gather all changes for this object (and its related objects)
content_type = ContentType.objects.get_for_model(model)
journalentries = JournalEntry.objects.restrict(request.user, 'view').prefetch_related('created_by').filter(
assigned_object_type=content_type,
assigned_object_id=obj.pk
)
journalentry_table = tables.ObjectJournalTable(journalentries)
journalentry_table.configure(request)
if request.user.has_perm('extras.add_journalentry'):
form = forms.JournalEntryForm(
initial={
'assigned_object_type': ContentType.objects.get_for_model(obj),
'assigned_object_id': obj.pk
}
)
else:
form = None
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
# fall back to using base.html.
if self.base_template is None:
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
return render(request, 'extras/object_journal.html', {
'object': obj,
'form': form,
'table': journalentry_table,
'base_template': self.base_template,
'active_tab': 'journal',
})
# #
# Reports # Reports
# #

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from extras.views import ObjectChangeLogView, ObjectJournalView from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from . import views from . import views
from .models import * from .models import *

View File

@ -1,2 +1,3 @@
from .object_views import *
from .bulk_views import * from .bulk_views import *
from .feature_views import *
from .object_views import *

View File

@ -0,0 +1,112 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.shortcuts import get_object_or_404, render
from django.views.generic import View
from extras import forms, tables
from extras.models import *
__all__ = (
'ObjectChangeLogView',
'ObjectJournalView',
)
class ObjectChangeLogView(View):
"""
Present a history of changes made to a particular object. The model class must be passed as a keyword argument
when referencing this view in a URL path. For example:
path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}),
Attributes:
base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used.
"""
base_template = None
def get(self, request, model, **kwargs):
# Handle QuerySet restriction of parent object if needed
if hasattr(model.objects, 'restrict'):
obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
else:
obj = get_object_or_404(model, **kwargs)
# Gather all changes for this object (and its related objects)
content_type = ContentType.objects.get_for_model(model)
objectchanges = ObjectChange.objects.restrict(request.user, 'view').prefetch_related(
'user', 'changed_object_type'
).filter(
Q(changed_object_type=content_type, changed_object_id=obj.pk) |
Q(related_object_type=content_type, related_object_id=obj.pk)
)
objectchanges_table = tables.ObjectChangeTable(
data=objectchanges,
orderable=False
)
objectchanges_table.configure(request)
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
# fall back to using base.html.
if self.base_template is None:
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
return render(request, 'extras/object_changelog.html', {
'object': obj,
'table': objectchanges_table,
'base_template': self.base_template,
'active_tab': 'changelog',
})
class ObjectJournalView(View):
"""
Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this
view in a URL path. For example:
path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}),
Attributes:
base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used.
"""
base_template = None
def get(self, request, model, **kwargs):
# Handle QuerySet restriction of parent object if needed
if hasattr(model.objects, 'restrict'):
obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs)
else:
obj = get_object_or_404(model, **kwargs)
# Gather all changes for this object (and its related objects)
content_type = ContentType.objects.get_for_model(model)
journalentries = JournalEntry.objects.restrict(request.user, 'view').prefetch_related('created_by').filter(
assigned_object_type=content_type,
assigned_object_id=obj.pk
)
journalentry_table = tables.ObjectJournalTable(journalentries)
journalentry_table.configure(request)
if request.user.has_perm('extras.add_journalentry'):
form = forms.JournalEntryForm(
initial={
'assigned_object_type': ContentType.objects.get_for_model(obj),
'assigned_object_id': obj.pk
}
)
else:
form = None
# Default to using "<app>/<model>.html" as the template, if it exists. Otherwise,
# fall back to using base.html.
if self.base_template is None:
self.base_template = f"{model._meta.app_label}/{model._meta.model_name}.html"
return render(request, 'extras/object_journal.html', {
'object': obj,
'form': form,
'table': journalentry_table,
'base_template': self.base_template,
'active_tab': 'journal',
})

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from extras.views import ObjectChangeLogView, ObjectJournalView from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from . import views from . import views
from .models import * from .models import *

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from extras.views import ObjectChangeLogView, ObjectJournalView from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from . import views from . import views
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from extras.views import ObjectChangeLogView, ObjectJournalView from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
from . import views from . import views
from .models import * from .models import *