diff --git a/docs/development/utility-views.md b/docs/development/utility-views.md index a6e50f71e..3b9c1053d 100644 --- a/docs/development/utility-views.md +++ b/docs/development/utility-views.md @@ -4,6 +4,10 @@ Utility views are reusable views that handle common CRUD tasks, such as listing ## Individual Views +### ObjectView + +Retrieve and display a single object. + ### ObjectListView Generates a paginated table of objects from a given queryset, which may optionally be filtered. diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index e3260431f..1f5f05230 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -1,18 +1,16 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import permission_required -from django.contrib.auth.mixins import PermissionRequiredMixin from django.db import transaction from django.db.models import Count, OuterRef from django.shortcuts import get_object_or_404, redirect, render -from django.views.generic import View from django_tables2 import RequestConfig from extras.models import Graph from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.views import ( - BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, + BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables from .choices import CircuitTerminationSideChoices @@ -30,12 +28,12 @@ class ProviderListView(ObjectListView): table = tables.ProviderTable -class ProviderView(PermissionRequiredMixin, View): - permission_required = 'circuits.view_provider' +class ProviderView(ObjectView): + queryset = Provider.objects.all() def get(self, request, slug): - provider = get_object_or_404(Provider, slug=slug) + provider = get_object_or_404(self.queryset, slug=slug) circuits = Circuit.objects.filter( provider=provider ).prefetch_related( @@ -135,12 +133,12 @@ class CircuitListView(ObjectListView): table = tables.CircuitTable -class CircuitView(PermissionRequiredMixin, View): - permission_required = 'circuits.view_circuit' +class CircuitView(ObjectView): + queryset = Circuit.objects.prefetch_related('provider', 'type', 'tenant__group') def get(self, request, pk): - circuit = get_object_or_404(Circuit.objects.prefetch_related('provider', 'type', 'tenant__group'), pk=pk) + circuit = get_object_or_404(self.queryset, pk=pk) termination_a = CircuitTermination.objects.prefetch_related( 'site__region', 'connected_endpoint__device' ).filter( diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5559d577c..fb60b6b31 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -26,7 +26,7 @@ from utilities.paginator import EnhancedPaginator from utilities.utils import csv_format from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin, - ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin, + ObjectView, ObjectImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ObjectPermissionRequiredMixin, ) from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -185,8 +185,7 @@ class SiteListView(ObjectListView): table = tables.SiteTable -class SiteView(ObjectPermissionRequiredMixin, View): - permission_required = 'dcim.view_site' +class SiteView(ObjectView): queryset = Site.objects.prefetch_related('region', 'tenant__group') def get(self, request, slug): @@ -362,12 +361,12 @@ class RackElevationListView(PermissionRequiredMixin, View): }) -class RackView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_rack' +class RackView(ObjectView): + queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role') def get(self, request, pk): - rack = get_object_or_404(Rack.objects.prefetch_related('site__region', 'tenant__group', 'group', 'role'), pk=pk) + rack = get_object_or_404(self.queryset, pk=pk) nonracked_devices = Device.objects.filter( rack=rack, @@ -440,12 +439,12 @@ class RackReservationListView(ObjectListView): action_buttons = ('export',) -class RackReservationView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_rackreservation' +class RackReservationView(ObjectView): + queryset = RackReservation.objects.prefetch_related('rack') def get(self, request, pk): - rackreservation = get_object_or_404(RackReservation.objects.prefetch_related('rack'), pk=pk) + rackreservation = get_object_or_404(self.queryset, pk=pk) return render(request, 'dcim/rackreservation.html', { 'rackreservation': rackreservation, @@ -546,12 +545,12 @@ class DeviceTypeListView(ObjectListView): table = tables.DeviceTypeTable -class DeviceTypeView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_devicetype' +class DeviceTypeView(ObjectView): + queryset = DeviceType.objects.prefetch_related('manufacturer') def get(self, request, pk): - devicetype = get_object_or_404(DeviceType, pk=pk) + devicetype = get_object_or_404(self.queryset, pk=pk) # Component tables consoleport_table = tables.ConsolePortTemplateTable( @@ -990,14 +989,14 @@ class DeviceListView(ObjectListView): template_name = 'dcim/device_list.html' -class DeviceView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_device' +class DeviceView(ObjectView): + queryset = Device.objects.prefetch_related( + 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' + ) def get(self, request, pk): - device = get_object_or_404(Device.objects.prefetch_related( - 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform' - ), pk=pk) + device = get_object_or_404(self.queryset, pk=pk) # VirtualChassis members if device.virtual_chassis is not None: @@ -1068,12 +1067,12 @@ class DeviceView(PermissionRequiredMixin, View): }) -class DeviceInventoryView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_device' +class DeviceInventoryView(ObjectView): + queryset = Device.objects.all() def get(self, request, pk): - device = get_object_or_404(Device, pk=pk) + device = get_object_or_404(self.queryset, pk=pk) inventory_items = InventoryItem.objects.filter( device=device, parent=None ).prefetch_related( @@ -1087,12 +1086,13 @@ class DeviceInventoryView(PermissionRequiredMixin, View): }) -class DeviceStatusView(PermissionRequiredMixin, View): +class DeviceStatusView(ObjectView): permission_required = ('dcim.view_device', 'dcim.napalm_read') + queryset = Device.objects.all() def get(self, request, pk): - device = get_object_or_404(Device, pk=pk) + device = get_object_or_404(self.queryset, pk=pk) return render(request, 'dcim/device_status.html', { 'device': device, @@ -1102,10 +1102,11 @@ class DeviceStatusView(PermissionRequiredMixin, View): class DeviceLLDPNeighborsView(PermissionRequiredMixin, View): permission_required = ('dcim.view_device', 'dcim.napalm_read') + queryset = Device.objects.all() def get(self, request, pk): - device = get_object_or_404(Device, pk=pk) + device = get_object_or_404(self.queryset, pk=pk) interfaces = device.vc_interfaces.exclude(type__in=NONCONNECTABLE_IFACE_TYPES).prefetch_related( '_connected_interface__device' ) @@ -1119,10 +1120,11 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View): class DeviceConfigView(PermissionRequiredMixin, View): permission_required = ('dcim.view_device', 'dcim.napalm_read') + queryset = Device.objects.all() def get(self, request, pk): - device = get_object_or_404(Device, pk=pk) + device = get_object_or_404(self.queryset, pk=pk) return render(request, 'dcim/device_config.html', { 'device': device, @@ -1426,12 +1428,12 @@ class InterfaceListView(ObjectListView): action_buttons = ('import', 'export') -class InterfaceView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_interface' +class InterfaceView(ObjectView): + queryset = Interface.objects.all() def get(self, request, pk): - interface = get_object_or_404(Interface, pk=pk) + interface = get_object_or_404(self.queryset, pk=pk) # Get assigned IP addresses ipaddress_table = InterfaceIPAddressTable( @@ -1878,12 +1880,12 @@ class CableListView(ObjectListView): action_buttons = ('import', 'export') -class CableView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_cable' +class CableView(ObjectView): + queryset = Cable.objects.all() def get(self, request, pk): - cable = get_object_or_404(Cable, pk=pk) + cable = get_object_or_404(self.queryset, pk=pk) return render(request, 'dcim/cable.html', { 'cable': cable, @@ -2194,11 +2196,11 @@ class VirtualChassisListView(ObjectListView): action_buttons = ('export',) -class VirtualChassisView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_virtualchassis' +class VirtualChassisView(ObjectView): + queryset = VirtualChassis.objects.prefetch_related('members') def get(self, request, pk): - virtualchassis = get_object_or_404(VirtualChassis.objects.prefetch_related('members'), pk=pk) + virtualchassis = get_object_or_404(self.queryset, pk=pk) return render(request, 'dcim/virtualchassis.html', { 'virtualchassis': virtualchassis, @@ -2461,12 +2463,12 @@ class PowerPanelListView(ObjectListView): table = tables.PowerPanelTable -class PowerPanelView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_powerpanel' +class PowerPanelView(ObjectView): + queryset = PowerPanel.objects.prefetch_related('site', 'rack_group') def get(self, request, pk): - powerpanel = get_object_or_404(PowerPanel.objects.prefetch_related('site', 'rack_group'), pk=pk) + powerpanel = get_object_or_404(self.queryset, pk=pk) powerfeed_table = tables.PowerFeedTable( data=PowerFeed.objects.filter(power_panel=powerpanel).prefetch_related('rack'), orderable=False @@ -2529,12 +2531,12 @@ class PowerFeedListView(ObjectListView): table = tables.PowerFeedTable -class PowerFeedView(PermissionRequiredMixin, View): - permission_required = 'dcim.view_powerfeed' +class PowerFeedView(ObjectView): + queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack') def get(self, request, pk): - powerfeed = get_object_or_404(PowerFeed.objects.prefetch_related('power_panel', 'rack'), pk=pk) + powerfeed = get_object_or_404(self.queryset, pk=pk) return render(request, 'dcim/powerfeed.html', { 'powerfeed': powerfeed, diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 0a3796a28..78db8f24a 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -13,7 +13,7 @@ from django_tables2 import RequestConfig from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.utils import shallow_compare_dict -from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView +from utilities.views import BulkDeleteView, BulkEditView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView from . import filters, forms from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem from .reports import get_report, get_reports @@ -37,12 +37,12 @@ class TagListView(ObjectListView): action_buttons = () -class TagView(PermissionRequiredMixin, View): - permission_required = 'extras.view_tag' +class TagView(ObjectView): + queryset = Tag.objects.all() def get(self, request, slug): - tag = get_object_or_404(Tag, slug=slug) + tag = get_object_or_404(self.queryset, slug=slug) tagged_items = TaggedItem.objects.filter( tag=tag ).prefetch_related( @@ -109,11 +109,11 @@ class ConfigContextListView(ObjectListView): action_buttons = ('add',) -class ConfigContextView(PermissionRequiredMixin, View): - permission_required = 'extras.view_configcontext' +class ConfigContextView(ObjectView): + queryset = ConfigContext.objects.all() def get(self, request, pk): - configcontext = get_object_or_404(ConfigContext, pk=pk) + configcontext = get_object_or_404(self.queryset, pk=pk) # Determine user's preferred output format if request.GET.get('format') in ['json', 'yaml']: @@ -195,12 +195,12 @@ class ObjectChangeListView(ObjectListView): action_buttons = ('export',) -class ObjectChangeView(PermissionRequiredMixin, View): - permission_required = 'extras.view_objectchange' +class ObjectChangeView(ObjectView): + queryset = ObjectChange.objects.all() def get(self, request, pk): - objectchange = get_object_or_404(ObjectChange, pk=pk) + objectchange = get_object_or_404(self.queryset, pk=pk) related_changes = ObjectChange.objects.filter(request_id=objectchange.request_id).exclude(pk=objectchange.pk) related_changes_table = ObjectChangeTable( diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 19d38be5d..706f819cc 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -10,8 +10,8 @@ from django_tables2 import RequestConfig from dcim.models import Device, Interface from utilities.paginator import EnhancedPaginator from utilities.views import ( - BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, - ObjectPermissionRequiredMixin, + BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, + ObjectListView, ) from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -120,12 +120,12 @@ class VRFListView(ObjectListView): table = tables.VRFTable -class VRFView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_vrf' +class VRFView(ObjectView): + queryset = VRF.objects.all() def get(self, request, pk): - vrf = get_object_or_404(VRF.objects.all(), pk=pk) + vrf = get_object_or_404(self.queryset, pk=pk) prefix_count = Prefix.objects.filter(vrf=vrf).count() return render(request, 'ipam/vrf.html', { @@ -298,12 +298,12 @@ class AggregateListView(ObjectListView): } -class AggregateView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_aggregate' +class AggregateView(ObjectView): + queryset = Aggregate.objects.all() def get(self, request, pk): - aggregate = get_object_or_404(Aggregate, pk=pk) + aggregate = get_object_or_404(self.queryset, pk=pk) # Find all child prefixes contained by this aggregate child_prefixes = Prefix.objects.filter( @@ -422,8 +422,7 @@ class PrefixListView(ObjectListView): return self.queryset.annotate_depth(limit=limit) -class PrefixView(ObjectPermissionRequiredMixin, View): - permission_required = 'ipam.view_prefix' +class PrefixView(ObjectView): queryset = Prefix.objects.prefetch_related('vrf', 'site__region', 'tenant__group', 'vlan__group', 'role') def get(self, request, pk): @@ -465,12 +464,12 @@ class PrefixView(ObjectPermissionRequiredMixin, View): }) -class PrefixPrefixesView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_prefix' +class PrefixPrefixesView(ObjectView): + queryset = Prefix.objects.all() def get(self, request, pk): - prefix = get_object_or_404(Prefix.objects.all(), pk=pk) + prefix = get_object_or_404(self.queryset, pk=pk) # Child prefixes table child_prefixes = prefix.get_child_prefixes().prefetch_related( @@ -509,12 +508,12 @@ class PrefixPrefixesView(PermissionRequiredMixin, View): }) -class PrefixIPAddressesView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_prefix' +class PrefixIPAddressesView(ObjectView): + queryset = Prefix.objects.all() def get(self, request, pk): - prefix = get_object_or_404(Prefix.objects.all(), pk=pk) + prefix = get_object_or_404(self.queryset, pk=pk) # Find all IPAddresses belonging to this Prefix ipaddresses = prefix.get_child_ips().prefetch_related( @@ -601,12 +600,12 @@ class IPAddressListView(ObjectListView): table = tables.IPAddressDetailTable -class IPAddressView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_ipaddress' +class IPAddressView(ObjectView): + queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant') def get(self, request, pk): - ipaddress = get_object_or_404(IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), pk=pk) + ipaddress = get_object_or_404(self.queryset, pk=pk) # Parent prefixes table parent_prefixes = Prefix.objects.filter( @@ -833,14 +832,12 @@ class VLANListView(ObjectListView): table = tables.VLANDetailTable -class VLANView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_vlan' +class VLANView(ObjectView): + queryset = VLAN.objects.prefetch_related('site__region', 'tenant__group', 'role') def get(self, request, pk): - vlan = get_object_or_404(VLAN.objects.prefetch_related( - 'site__region', 'tenant__group', 'role' - ), pk=pk) + vlan = get_object_or_404(self.queryset, pk=pk) prefixes = Prefix.objects.filter(vlan=vlan).prefetch_related('vrf', 'site', 'role') prefix_table = tables.PrefixTable(list(prefixes), orderable=False) prefix_table.exclude = ('vlan',) @@ -851,12 +848,12 @@ class VLANView(PermissionRequiredMixin, View): }) -class VLANMembersView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_vlan' +class VLANMembersView(ObjectView): + queryset = VLAN.objects.all() def get(self, request, pk): - vlan = get_object_or_404(VLAN.objects.all(), pk=pk) + vlan = get_object_or_404(self.queryset, pk=pk) members = vlan.get_members().prefetch_related('device', 'virtual_machine') members_table = tables.VLANMemberTable(members) @@ -920,12 +917,12 @@ class ServiceListView(ObjectListView): action_buttons = ('export',) -class ServiceView(PermissionRequiredMixin, View): - permission_required = 'ipam.view_service' +class ServiceView(ObjectView): + queryset = Service.objects.all() def get(self, request, pk): - service = get_object_or_404(Service, pk=pk) + service = get_object_or_404(self.queryset, pk=pk) return render(request, 'ipam/service.html', { 'service': service, diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index dbcf72262..a2e627a7c 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -9,7 +9,8 @@ from django.urls import reverse from django.views.generic import View from utilities.views import ( - BulkDeleteView, BulkEditView, BulkImportView, GetReturnURLMixin, ObjectDeleteView, ObjectEditView, ObjectListView, + BulkDeleteView, BulkEditView, BulkImportView, GetReturnURLMixin, ObjectView, ObjectDeleteView, ObjectEditView, + ObjectListView, ) from . import filters, forms, tables from .decorators import userkey_required @@ -66,12 +67,12 @@ class SecretListView(ObjectListView): action_buttons = ('import', 'export') -class SecretView(PermissionRequiredMixin, View): - permission_required = 'secrets.view_secret' +class SecretView(ObjectView): + queryset = Secret.objects.all() def get(self, request, pk): - secret = get_object_or_404(Secret, pk=pk) + secret = get_object_or_404(self.queryset, pk=pk) return render(request, 'secrets/secret.html', { 'secret': secret, diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 3de321301..823df9933 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -1,13 +1,11 @@ -from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Count from django.shortcuts import get_object_or_404, render -from django.views.generic import View from circuits.models import Circuit from dcim.models import Site, Rack, Device, RackReservation from ipam.models import IPAddress, Prefix, VLAN, VRF from utilities.views import ( - BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, + BulkDeleteView, BulkEditView, BulkImportView, ObjectView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from virtualization.models import VirtualMachine, Cluster from . import filters, forms, tables @@ -59,12 +57,12 @@ class TenantListView(ObjectListView): table = tables.TenantTable -class TenantView(PermissionRequiredMixin, View): - permission_required = 'tenancy.view_tenant' +class TenantView(ObjectView): + queryset = Tenant.objects.prefetch_related('group') def get(self, request, slug): - tenant = get_object_or_404(Tenant, slug=slug) + tenant = get_object_or_404(self.queryset, slug=slug) stats = { 'site_count': Site.objects.filter(tenant=tenant).count(), 'rack_count': Rack.objects.filter(tenant=tenant).count(), diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 6a1086c94..bd612b4df 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -118,6 +118,18 @@ class GetReturnURLMixin(object): # Generic views # +class ObjectView(ObjectPermissionRequiredMixin, View): + """ + Retrieve a single object for display. + + :param queryset: The base queryset for retrieving the object. + """ + queryset = None + + def get_required_permission(self): + return get_permission_for_model(self.queryset.model, 'view') + + class ObjectListView(ObjectPermissionRequiredMixin, View): """ List a series of objects. diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 898648f90..53fcf9697 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -11,8 +11,8 @@ from dcim.tables import DeviceTable from extras.views import ObjectConfigContextView from ipam.models import Service from utilities.views import ( - BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectDeleteView, - ObjectEditView, ObjectListView, + BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectView, + ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -85,12 +85,12 @@ class ClusterListView(ObjectListView): filterset_form = forms.ClusterFilterForm -class ClusterView(PermissionRequiredMixin, View): - permission_required = 'virtualization.view_cluster' +class ClusterView(ObjectView): + queryset = Cluster.objects.all() def get(self, request, pk): - cluster = get_object_or_404(Cluster, pk=pk) + cluster = get_object_or_404(self.queryset, pk=pk) devices = Device.objects.filter(cluster=cluster).prefetch_related( 'site', 'rack', 'tenant', 'device_type__manufacturer' ) @@ -233,12 +233,12 @@ class VirtualMachineListView(ObjectListView): template_name = 'virtualization/virtualmachine_list.html' -class VirtualMachineView(PermissionRequiredMixin, View): - permission_required = 'virtualization.view_virtualmachine' +class VirtualMachineView(ObjectView): + queryset = VirtualMachine.objects.prefetch_related('tenant__group') def get(self, request, pk): - virtualmachine = get_object_or_404(VirtualMachine.objects.prefetch_related('tenant__group'), pk=pk) + virtualmachine = get_object_or_404(self.queryset, pk=pk) interfaces = Interface.objects.filter(virtual_machine=virtualmachine) services = Service.objects.filter(virtual_machine=virtualmachine)