diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index 5408cef19..17a392814 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -146,9 +146,9 @@ A `PluginMenuButton` has the following attributes: !!! 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. -## 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 rendering: @@ -177,9 +177,9 @@ Below is the class definition for NetBox's BaseObjectView. The attributes and me rendering: 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 rendering: @@ -212,3 +212,21 @@ Below is the class definition for NetBox's BaseMultiObjectView. The attributes a - get_form rendering: 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 diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index e634eeeb4..7feeb28f6 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -1,7 +1,7 @@ from django.urls import path from dcim.views import CableCreateView, PathTraceView -from extras.views import ObjectChangeLogView, ObjectJournalView +from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from . import views from .models import * diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index bfd6fecad..c5cd0fa65 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from extras.views import ObjectChangeLogView, ObjectJournalView +from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from . import views from .models import * diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index de8ef1531..4c23adb0f 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -1,6 +1,7 @@ from django.urls import path from extras import models, views +from netbox.views.generic import ObjectChangeLogView app_name = 'extras' @@ -15,7 +16,7 @@ urlpatterns = [ path('custom-fields//', views.CustomFieldView.as_view(), name='customfield'), path('custom-fields//edit/', views.CustomFieldEditView.as_view(), name='customfield_edit'), path('custom-fields//delete/', views.CustomFieldDeleteView.as_view(), name='customfield_delete'), - path('custom-fields//changelog/', views.ObjectChangeLogView.as_view(), name='customfield_changelog', + path('custom-fields//changelog/', ObjectChangeLogView.as_view(), name='customfield_changelog', kwargs={'model': models.CustomField}), # Custom links @@ -27,7 +28,7 @@ urlpatterns = [ path('custom-links//', views.CustomLinkView.as_view(), name='customlink'), path('custom-links//edit/', views.CustomLinkEditView.as_view(), name='customlink_edit'), path('custom-links//delete/', views.CustomLinkDeleteView.as_view(), name='customlink_delete'), - path('custom-links//changelog/', views.ObjectChangeLogView.as_view(), name='customlink_changelog', + path('custom-links//changelog/', ObjectChangeLogView.as_view(), name='customlink_changelog', kwargs={'model': models.CustomLink}), # Export templates @@ -39,7 +40,7 @@ urlpatterns = [ path('export-templates//', views.ExportTemplateView.as_view(), name='exporttemplate'), path('export-templates//edit/', views.ExportTemplateEditView.as_view(), name='exporttemplate_edit'), path('export-templates//delete/', views.ExportTemplateDeleteView.as_view(), name='exporttemplate_delete'), - path('export-templates//changelog/', views.ObjectChangeLogView.as_view(), name='exporttemplate_changelog', + path('export-templates//changelog/', ObjectChangeLogView.as_view(), name='exporttemplate_changelog', kwargs={'model': models.ExportTemplate}), # Webhooks @@ -51,7 +52,7 @@ urlpatterns = [ path('webhooks//', views.WebhookView.as_view(), name='webhook'), path('webhooks//edit/', views.WebhookEditView.as_view(), name='webhook_edit'), path('webhooks//delete/', views.WebhookDeleteView.as_view(), name='webhook_delete'), - path('webhooks//changelog/', views.ObjectChangeLogView.as_view(), name='webhook_changelog', + path('webhooks//changelog/', ObjectChangeLogView.as_view(), name='webhook_changelog', kwargs={'model': models.Webhook}), # Tags @@ -63,7 +64,7 @@ urlpatterns = [ path('tags//', views.TagView.as_view(), name='tag'), path('tags//edit/', views.TagEditView.as_view(), name='tag_edit'), path('tags//delete/', views.TagDeleteView.as_view(), name='tag_delete'), - path('tags//changelog/', views.ObjectChangeLogView.as_view(), name='tag_changelog', + path('tags//changelog/', ObjectChangeLogView.as_view(), name='tag_changelog', kwargs={'model': models.Tag}), # Config contexts @@ -74,7 +75,7 @@ urlpatterns = [ path('config-contexts//', views.ConfigContextView.as_view(), name='configcontext'), path('config-contexts//edit/', views.ConfigContextEditView.as_view(), name='configcontext_edit'), path('config-contexts//delete/', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), - path('config-contexts//changelog/', views.ObjectChangeLogView.as_view(), name='configcontext_changelog', + path('config-contexts//changelog/', ObjectChangeLogView.as_view(), name='configcontext_changelog', kwargs={'model': models.ConfigContext}), # Image attachments @@ -90,7 +91,7 @@ urlpatterns = [ path('journal-entries//', views.JournalEntryView.as_view(), name='journalentry'), path('journal-entries//edit/', views.JournalEntryEditView.as_view(), name='journalentry_edit'), path('journal-entries//delete/', views.JournalEntryDeleteView.as_view(), name='journalentry_delete'), - path('journal-entries//changelog/', views.ObjectChangeLogView.as_view(), name='journalentry_changelog', + path('journal-entries//changelog/', ObjectChangeLogView.as_view(), name='journalentry_changelog', kwargs={'model': models.JournalEntry}), # Change logging diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 98ad51592..903cdea64 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -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, "/.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 "/.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 # @@ -547,55 +504,6 @@ class JournalEntryBulkDeleteView(generic.BulkDeleteView): 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, "/.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 "/.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 # diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 0a4eddc6c..3c7ed2d1f 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from extras.views import ObjectChangeLogView, ObjectJournalView +from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from . import views from .models import * diff --git a/netbox/netbox/views/generic/__init__.py b/netbox/netbox/views/generic/__init__.py index 9dd933568..8b2a86185 100644 --- a/netbox/netbox/views/generic/__init__.py +++ b/netbox/netbox/views/generic/__init__.py @@ -1,2 +1,3 @@ -from .object_views import * from .bulk_views import * +from .feature_views import * +from .object_views import * diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py new file mode 100644 index 000000000..7a75e74ec --- /dev/null +++ b/netbox/netbox/views/generic/feature_views.py @@ -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//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 "/.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//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 "/.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', + }) diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index 6b3565bfb..214100275 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from extras.views import ObjectChangeLogView, ObjectJournalView +from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from . import views from .models import * diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index bfc5fe6c2..e01dbc059 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from extras.views import ObjectChangeLogView, ObjectJournalView +from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from . import views from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface diff --git a/netbox/wireless/urls.py b/netbox/wireless/urls.py index 684f55ad5..cef96fd5e 100644 --- a/netbox/wireless/urls.py +++ b/netbox/wireless/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from extras.views import ObjectChangeLogView, ObjectJournalView +from netbox.views.generic import ObjectChangeLogView, ObjectJournalView from . import views from .models import *