From 78f741f85661267441ec5b4c89a05183d0db373f Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Sun, 28 Apr 2024 14:25:15 +0200 Subject: [PATCH 1/6] Closes #15794: Make "related objects" dynamic Instead of hardcoding relationships between models for the detail view, they are now dynamically generated. --- netbox/circuits/views.py | 36 +++----- netbox/core/views.py | 10 +-- netbox/dcim/views.py | 160 ++++++++++++--------------------- netbox/ipam/views.py | 46 +++------- netbox/tenancy/views.py | 35 +++----- netbox/utilities/views.py | 49 ++++++++++ netbox/virtualization/views.py | 18 ++-- netbox/vpn/views.py | 10 +-- netbox/wireless/views.py | 9 +- 9 files changed, 151 insertions(+), 222 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 64dd8268200..415812e4153 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -7,7 +7,7 @@ from tenancy.views import ObjectContactsView from utilities.forms import ConfirmationForm from utilities.utils import count_related -from utilities.views import register_model_view +from utilities.views import GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables from .models import * @@ -26,17 +26,12 @@ class ProviderListView(generic.ObjectListView): @register_model_view(Provider) -class ProviderView(generic.ObjectView): +class ProviderView(GetRelatedModelsMixin, generic.ObjectView): queryset = Provider.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'), - (Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -92,16 +87,12 @@ class ProviderAccountListView(generic.ObjectListView): @register_model_view(ProviderAccount) -class ProviderAccountView(generic.ObjectView): +class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView): queryset = ProviderAccount.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -156,19 +147,20 @@ class ProviderNetworkListView(generic.ObjectListView): @register_model_view(ProviderNetwork) -class ProviderNetworkView(generic.ObjectView): +class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView): queryset = ProviderNetwork.objects.all() - def get_extra_context(self, request, instance): - related_models = ( + def get_extra_related_models(self, request, instance): + return ( ( Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), 'provider_network_id', ), ) + def get_extra_context(self, request, instance): return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance, [CircuitTermination]), } @@ -215,16 +207,12 @@ class CircuitTypeListView(generic.ObjectListView): @register_model_view(CircuitType) -class CircuitTypeView(generic.ObjectView): +class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView): queryset = CircuitType.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } diff --git a/netbox/core/views.py b/netbox/core/views.py index 6c87087f2e4..543ecee6bf5 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -8,7 +8,7 @@ from netbox.views import generic from netbox.views.generic.base import BaseObjectView from utilities.utils import count_related -from utilities.views import ContentTypePermissionRequiredMixin, register_model_view +from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables from .models import * @@ -27,16 +27,12 @@ class DataSourceListView(generic.ObjectListView): @register_model_view(DataSource) -class DataSourceView(generic.ObjectView): +class DataSourceView(GetRelatedModelsMixin, generic.ObjectView): queryset = DataSource.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index ce4bb5750d8..d568abac606 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -27,7 +27,9 @@ from utilities.permissions import get_permission_for_model from utilities.query_functions import CollateAsChar from utilities.utils import count_related -from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view +from utilities.views import ( + GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view, +) from virtualization.models import VirtualMachine from . import filtersets, forms, tables from .choices import DeviceFaceChoices @@ -224,19 +226,20 @@ class RegionListView(generic.ObjectListView): @register_model_view(Region) -class RegionView(generic.ObjectView): +class RegionView(GetRelatedModelsMixin, generic.ObjectView): queryset = Region.objects.all() - def get_extra_context(self, request, instance): - regions = instance.get_descendants(include_self=True) - related_models = ( - (Site.objects.restrict(request.user, 'view').filter(region__in=regions), 'region_id'), + def get_extra_related_models(self, request, regions): + return ( (Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'), (Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'), ) + def get_extra_context(self, request, instance): + regions = instance.get_descendants(include_self=True) + return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, regions), } @@ -304,19 +307,20 @@ class SiteGroupListView(generic.ObjectListView): @register_model_view(SiteGroup) -class SiteGroupView(generic.ObjectView): +class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = SiteGroup.objects.all() - def get_extra_context(self, request, instance): - groups = instance.get_descendants(include_self=True) - related_models = ( - (Site.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'), + def get_extra_related_models(self, request, groups): + return ( (Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'), (Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'), ) + def get_extra_context(self, request, instance): + groups = instance.get_descendants(include_self=True) + return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, groups), } @@ -378,31 +382,21 @@ class SiteListView(generic.ObjectListView): @register_model_view(Site) -class SiteView(generic.ObjectView): +class SiteView(GetRelatedModelsMixin, generic.ObjectView): queryset = Site.objects.prefetch_related('tenant__group') - def get_extra_context(self, request, instance): - related_models = ( - # DCIM - (Location.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), - (Rack.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), - (Device.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), - # Virtualization - (VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=instance), 'site_id'), - # IPAM - (Prefix.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), - (ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'), + def get_extra_related_models(self, request, instance): + return ( (VLANGroup.objects.restrict(request.user, 'view').filter( scope_type=ContentType.objects.get_for_model(Site), scope_id=instance.pk ), 'site'), - (VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), - # Circuits (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'), ) + def get_extra_context(self, request, instance): return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance, [CableTermination, CircuitTermination]), } @@ -464,18 +458,13 @@ class LocationListView(generic.ObjectListView): @register_model_view(Location) -class LocationView(generic.ObjectView): +class LocationView(GetRelatedModelsMixin, generic.ObjectView): queryset = Location.objects.all() def get_extra_context(self, request, instance): locations = instance.get_descendants(include_self=True) - related_models = ( - (Rack.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'), - (Device.objects.restrict(request.user, 'view').filter(location__in=locations), 'location_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, locations), } @@ -539,16 +528,12 @@ class RackRoleListView(generic.ObjectListView): @register_model_view(RackRole) -class RackRoleView(generic.ObjectView): +class RackRoleView(GetRelatedModelsMixin, generic.ObjectView): queryset = RackRole.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Rack.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -653,15 +638,10 @@ def get(self, request): @register_model_view(Rack) -class RackView(generic.ObjectView): +class RackView(GetRelatedModelsMixin, generic.ObjectView): queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role') def get_extra_context(self, request, instance): - related_models = ( - (Device.objects.restrict(request.user, 'view').filter(rack=instance), 'rack_id'), - (PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'), - ) - peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site) if instance.location: @@ -677,7 +657,7 @@ def get_extra_context(self, request, instance): ]) return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), 'next_rack': next_rack, 'prev_rack': prev_rack, 'svg_extra': svg_extra, @@ -837,19 +817,12 @@ class ManufacturerListView(generic.ObjectListView): @register_model_view(Manufacturer) -class ManufacturerView(generic.ObjectView): +class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView): queryset = Manufacturer.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (DeviceType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'), - (ModuleType.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'), - (InventoryItem.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'), - (Platform.objects.restrict(request.user, 'view').filter(manufacturer=instance), 'manufacturer_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance, [InventoryItemTemplate]), } @@ -911,16 +884,16 @@ class DeviceTypeListView(generic.ObjectListView): @register_model_view(DeviceType) -class DeviceTypeView(generic.ObjectView): +class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView): queryset = DeviceType.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance, [ + ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, + InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, + RearPortTemplate, + ]), } @@ -1150,16 +1123,16 @@ class ModuleTypeListView(generic.ObjectListView): @register_model_view(ModuleType) -class ModuleTypeView(generic.ObjectView): +class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView): queryset = ModuleType.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance, [ + ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, + InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, + RearPortTemplate, + ]), } @@ -1712,17 +1685,12 @@ class DeviceRoleListView(generic.ObjectListView): @register_model_view(DeviceRole) -class DeviceRoleView(generic.ObjectView): +class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView): queryset = DeviceRole.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Device.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), - (VirtualMachine.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -1776,17 +1744,12 @@ class PlatformListView(generic.ObjectListView): @register_model_view(Platform) -class PlatformView(generic.ObjectView): +class PlatformView(GetRelatedModelsMixin, generic.ObjectView): queryset = Platform.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Device.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'), - (VirtualMachine.objects.restrict(request.user, 'view').filter(platform=instance), 'platform_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -2140,22 +2103,12 @@ class ModuleListView(generic.ObjectListView): @register_model_view(Module) -class ModuleView(generic.ObjectView): +class ModuleView(GetRelatedModelsMixin, generic.ObjectView): queryset = Module.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Interface.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), - (ConsolePort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), - (ConsoleServerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), - (PowerPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), - (PowerOutlet.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), - (FrontPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), - (RearPort.objects.restrict(request.user, 'view').filter(module=instance), 'module_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -3549,16 +3502,12 @@ class PowerPanelListView(generic.ObjectListView): @register_model_view(PowerPanel) -class PowerPanelView(generic.ObjectView): +class PowerPanelView(GetRelatedModelsMixin, generic.ObjectView): queryset = PowerPanel.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -3662,16 +3611,17 @@ class VirtualDeviceContextListView(generic.ObjectListView): @register_model_view(VirtualDeviceContext) -class VirtualDeviceContextView(generic.ObjectView): +class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView): queryset = VirtualDeviceContext.objects.all() - def get_extra_context(self, request, instance): - related_models = ( + def get_extra_related_models(self, request, instance): + return ( (Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'), ) + def get_extra_context(self, request, instance): return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 9c4a9a102e2..8c603f857a7 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -11,7 +11,7 @@ from netbox.views import generic from utilities.tables import get_table_ordering from utilities.utils import count_related -from utilities.views import ViewTab, register_model_view +from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view from virtualization.filtersets import VMInterfaceFilterSet from virtualization.models import VMInterface from . import filtersets, forms, tables @@ -33,15 +33,10 @@ class VRFListView(generic.ObjectListView): @register_model_view(VRF) -class VRFView(generic.ObjectView): +class VRFView(GetRelatedModelsMixin, generic.ObjectView): queryset = VRF.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Prefix.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'), - (IPAddress.objects.restrict(request.user, 'view').filter(vrf=instance), 'vrf_id'), - ) - import_targets_table = tables.RouteTargetTable( instance.import_targets.all(), orderable=False @@ -52,7 +47,7 @@ def get_extra_context(self, request, instance): ) return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), 'import_targets_table': import_targets_table, 'export_targets_table': export_targets_table, } @@ -146,16 +141,12 @@ class RIRListView(generic.ObjectListView): @register_model_view(RIR) -class RIRView(generic.ObjectView): +class RIRView(GetRelatedModelsMixin, generic.ObjectView): queryset = RIR.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Aggregate.objects.restrict(request.user, 'view').filter(rir=instance), 'rir_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -273,17 +264,12 @@ class ASNListView(generic.ObjectListView): @register_model_view(ASN) -class ASNView(generic.ObjectView): +class ASNView(GetRelatedModelsMixin, generic.ObjectView): queryset = ASN.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), - (Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, [instance]), } @@ -422,18 +408,12 @@ class RoleListView(generic.ObjectListView): @register_model_view(Role) -class RoleView(generic.ObjectView): +class RoleView(GetRelatedModelsMixin, generic.ObjectView): queryset = Role.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Prefix.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), - (IPRange.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), - (VLAN.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -907,16 +887,12 @@ class VLANGroupListView(generic.ObjectListView): @register_model_view(VLANGroup) -class VLANGroupView(generic.ObjectView): +class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') def get_extra_context(self, request, instance): - related_models = ( - (VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 1d2fceb04ec..5ac16039e10 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -3,8 +3,8 @@ from django.utils.translation import gettext as _ from netbox.views import generic -from utilities.utils import count_related, get_related_models -from utilities.views import register_model_view, ViewTab +from utilities.utils import count_related +from utilities.views import GetRelatedModelsMixin, register_model_view, ViewTab from . import filtersets, forms, tables from .models import * @@ -55,17 +55,14 @@ class TenantGroupListView(generic.ObjectListView): @register_model_view(TenantGroup) -class TenantGroupView(generic.ObjectView): +class TenantGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = TenantGroup.objects.all() def get_extra_context(self, request, instance): groups = instance.get_descendants(include_self=True) - related_models = ( - (Tenant.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'), - ) return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, groups), } @@ -122,17 +119,12 @@ class TenantListView(generic.ObjectListView): @register_model_view(Tenant) -class TenantView(generic.ObjectView): +class TenantView(GetRelatedModelsMixin, generic.ObjectView): queryset = Tenant.objects.all() def get_extra_context(self, request, instance): - related_models = [ - (model.objects.restrict(request.user, 'view').filter(tenant=instance), f'{field}_id') - for model, field in get_related_models(Tenant) - ] - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -188,17 +180,14 @@ class ContactGroupListView(generic.ObjectListView): @register_model_view(ContactGroup) -class ContactGroupView(generic.ObjectView): +class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = ContactGroup.objects.all() def get_extra_context(self, request, instance): groups = instance.get_descendants(include_self=True) - related_models = ( - (Contact.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'), - ) return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, groups), } @@ -255,16 +244,12 @@ class ContactRoleListView(generic.ObjectListView): @register_model_view(ContactRole) -class ContactRoleView(generic.ObjectView): +class ContactRoleView(GetRelatedModelsMixin, generic.ObjectView): queryset = ContactRole.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (ContactAssignment.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 9c89de99853..3a862b745c7 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -1,3 +1,5 @@ +from typing import Iterable + from django.contrib.auth.mixins import AccessMixin from django.core.exceptions import ImproperlyConfigured from django.urls import reverse @@ -5,10 +7,12 @@ from django.utils.translation import gettext_lazy as _ from netbox.registry import registry +from utilities.utils import get_related_models from .permissions import resolve_permission __all__ = ( 'ContentTypePermissionRequiredMixin', + 'GetRelatedModelsMixin', 'GetReturnURLMixin', 'ObjectPermissionRequiredMixin', 'ViewTab', @@ -140,6 +144,51 @@ def get_return_url(self, request, obj=None): return reverse('home') +class GetRelatedModelsMixin: + """ + Provides logic for collecting all related models for the currently viewed model. + """ + + def get_extra_related_models(self, request, instance): + """ + Get extra related models for `instance`, which extend `get_related_models`. Can be used to implement custom + lookups for nested and non-direct relationships. + """ + return [] + + def get_related_models(self, request, instance, omit=[]): + """ + Get related models of the view's `queryset` model without those listed in `omit`. Will be sorted alphabetical. + + Args: + request: Current request being processed. + instance: The instance related models should be looked up for. A list of instances can be passed to match + related objects in this list (e.g. to find sites of a region including child regions). + omit: Remove relationships to these models from the result. Needs to be passed, if related models don't + provide a `_list` view. + """ + model = self.queryset.model + related = filter( + lambda m: m[0] is not model and m[0] not in omit, + get_related_models(model, False) + ) + + related_models = [ + ( + model.objects.restrict(request.user, 'view').filter(**( + {f'{field}__in': instance} + if isinstance(instance, Iterable) + else {field: instance} + )), + f'{field}_id' + ) + for model, field in related + ] + related_models.extend(self.get_extra_related_models(request, instance)) + + return sorted(related_models, key=lambda x: x[0].model._meta.verbose_name.lower()) + + class ViewTab: """ ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index ec19a1d22f9..5f77128d3c6 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -20,7 +20,7 @@ from tenancy.views import ObjectContactsView from utilities.query_functions import CollateAsChar from utilities.utils import count_related -from utilities.views import ViewTab, register_model_view +from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view from . import filtersets, forms, tables from .models import * @@ -39,16 +39,12 @@ class ClusterTypeListView(generic.ObjectListView): @register_model_view(ClusterType) -class ClusterTypeView(generic.ObjectView): +class ClusterTypeView(GetRelatedModelsMixin, generic.ObjectView): queryset = ClusterType.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Cluster.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } @@ -99,16 +95,12 @@ class ClusterGroupListView(generic.ObjectListView): @register_model_view(ClusterGroup) -class ClusterGroupView(generic.ObjectView): +class ClusterGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = ClusterGroup.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Cluster.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, instance), } diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 9bf424af9b1..75b043e5686 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -2,7 +2,7 @@ from netbox.views import generic from tenancy.views import ObjectContactsView from utilities.utils import count_related -from utilities.views import register_model_view +from utilities.views import GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables from .models import * @@ -21,16 +21,12 @@ class TunnelGroupListView(generic.ObjectListView): @register_model_view(TunnelGroup) -class TunnelGroupView(generic.ObjectView): +class TunnelGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = TunnelGroup.objects.all() def get_extra_context(self, request, instance): - related_models = ( - (Tunnel.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'), - ) - return { - 'related_models': related_models, + 'related_models': self.get_related_models(instance), } diff --git a/netbox/wireless/views.py b/netbox/wireless/views.py index e1eb6fd7d58..0e8721b093c 100644 --- a/netbox/wireless/views.py +++ b/netbox/wireless/views.py @@ -1,7 +1,7 @@ from dcim.models import Interface from netbox.views import generic from utilities.utils import count_related -from utilities.views import register_model_view +from utilities.views import GetRelatedModelsMixin, register_model_view from . import filtersets, forms, tables from .models import * @@ -24,17 +24,14 @@ class WirelessLANGroupListView(generic.ObjectListView): @register_model_view(WirelessLANGroup) -class WirelessLANGroupView(generic.ObjectView): +class WirelessLANGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = WirelessLANGroup.objects.all() def get_extra_context(self, request, instance): groups = instance.get_descendants(include_self=True) - related_models = ( - (WirelessLAN.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'), - ) return { - 'related_models': related_models, + 'related_models': self.get_related_models(request, groups), } From f82e362d96563a747543831200d688fdf6a0d237 Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Sun, 28 Apr 2024 15:39:52 +0200 Subject: [PATCH 2/6] Fix related models call --- netbox/dcim/views.py | 4 ++-- netbox/vpn/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index d568abac606..772d7fff9f2 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -464,7 +464,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView): def get_extra_context(self, request, instance): locations = instance.get_descendants(include_self=True) return { - 'related_models': self.get_related_models(request, locations), + 'related_models': self.get_related_models(request, locations, [CableTermination]), } @@ -657,7 +657,7 @@ def get_extra_context(self, request, instance): ]) return { - 'related_models': self.get_related_models(request, instance), + 'related_models': self.get_related_models(request, instance, [CableTermination]), 'next_rack': next_rack, 'prev_rack': prev_rack, 'svg_extra': svg_extra, diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 75b043e5686..7e8507993a7 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -26,7 +26,7 @@ class TunnelGroupView(GetRelatedModelsMixin, generic.ObjectView): def get_extra_context(self, request, instance): return { - 'related_models': self.get_related_models(instance), + 'related_models': self.get_related_models(request, instance), } From 1218bf188bb89e8414d4a2ba1900ffaa27cff5cc Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Wed, 15 May 2024 23:05:21 +0200 Subject: [PATCH 3/6] Remove extra related models hook Instead of providing a rarely used hook method, additional related models can now be passed directly to the lookup method. --- netbox/circuits/views.py | 20 ++++++------ netbox/dcim/views.py | 65 +++++++++++++++++++++------------------ netbox/utilities/views.py | 13 +++----- 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 45084a3391c..f5129e5b876 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -150,17 +150,19 @@ class ProviderNetworkListView(generic.ObjectListView): class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView): queryset = ProviderNetwork.objects.all() - def get_extra_related_models(self, request, instance): - return ( - ( - Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), - 'provider_network_id', - ), - ) - def get_extra_context(self, request, instance): return { - 'related_models': self.get_related_models(request, instance, [CircuitTermination]), + 'related_models': self.get_related_models( + request, + instance, + [CircuitTermination], + ( + ( + Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), + 'provider_network_id', + ), + ), + ), } diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4f5bd69596e..ab8e350ca98 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -229,17 +229,18 @@ class RegionListView(generic.ObjectListView): class RegionView(GetRelatedModelsMixin, generic.ObjectView): queryset = Region.objects.all() - def get_extra_related_models(self, request, regions): - return ( - (Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'), - (Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'), - ) - def get_extra_context(self, request, instance): regions = instance.get_descendants(include_self=True) return { - 'related_models': self.get_related_models(request, regions), + 'related_models': self.get_related_models( + request, + regions, + extra=( + (Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'), + (Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'), + ), + ), } @@ -310,17 +311,18 @@ class SiteGroupListView(generic.ObjectListView): class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView): queryset = SiteGroup.objects.all() - def get_extra_related_models(self, request, groups): - return ( - (Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'), - (Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'), - ) - def get_extra_context(self, request, instance): groups = instance.get_descendants(include_self=True) return { - 'related_models': self.get_related_models(request, groups), + 'related_models': self.get_related_models( + request, + groups, + extra=( + (Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'), + (Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'), + ), + ), } @@ -385,18 +387,20 @@ class SiteListView(generic.ObjectListView): class SiteView(GetRelatedModelsMixin, generic.ObjectView): queryset = Site.objects.prefetch_related('tenant__group') - def get_extra_related_models(self, request, instance): - return ( - (VLANGroup.objects.restrict(request.user, 'view').filter( - scope_type=ContentType.objects.get_for_model(Site), - scope_id=instance.pk - ), 'site'), - (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'), - ) - def get_extra_context(self, request, instance): return { - 'related_models': self.get_related_models(request, instance, [CableTermination, CircuitTermination]), + 'related_models': self.get_related_models( + request, + instance, + [CableTermination, CircuitTermination], + ( + (VLANGroup.objects.restrict(request.user, 'view').filter( + scope_type=ContentType.objects.get_for_model(Site), + scope_id=instance.pk + ), 'site'), + (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'), + ), + ), } @@ -3598,14 +3602,15 @@ class VirtualDeviceContextListView(generic.ObjectListView): class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView): queryset = VirtualDeviceContext.objects.all() - def get_extra_related_models(self, request, instance): - return ( - (Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'), - ) - def get_extra_context(self, request, instance): return { - 'related_models': self.get_related_models(request, instance), + 'related_models': self.get_related_models( + request, + instance, + extra=( + (Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'), + ), + ), } diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 95df9020a95..75c48b01f72 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -151,14 +151,7 @@ class GetRelatedModelsMixin: Provides logic for collecting all related models for the currently viewed model. """ - def get_extra_related_models(self, request, instance): - """ - Get extra related models for `instance`, which extend `get_related_models`. Can be used to implement custom - lookups for nested and non-direct relationships. - """ - return [] - - def get_related_models(self, request, instance, omit=[]): + def get_related_models(self, request, instance, omit=[], extra=[]): """ Get related models of the view's `queryset` model without those listed in `omit`. Will be sorted alphabetical. @@ -168,6 +161,8 @@ def get_related_models(self, request, instance, omit=[]): related objects in this list (e.g. to find sites of a region including child regions). omit: Remove relationships to these models from the result. Needs to be passed, if related models don't provide a `_list` view. + extra: Add extra models to the list of automatically determined related models. Can be used to add indirect + relationships. """ model = self.queryset.model related = filter( @@ -186,7 +181,7 @@ def get_related_models(self, request, instance, omit=[]): ) for model, field in related ] - related_models.extend(self.get_extra_related_models(request, instance)) + related_models.extend(extra) return sorted(related_models, key=lambda x: x[0].model._meta.verbose_name.lower()) From 053fa9fdea9eb9c25ca83b564e489cda01315da3 Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Mon, 10 Jun 2024 21:47:01 +0200 Subject: [PATCH 4/6] Fix relations view for ASNs ASNs have ManyToMany relationships and therefore can't used automatic resolving. Explicit relations have been restored as before. --- netbox/dcim/views.py | 6 ++++-- netbox/ipam/views.py | 9 ++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 63927b5e7ed..8d6494c16ec 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -17,7 +17,7 @@ from circuits.models import Circuit, CircuitTermination from extras.views import ObjectConfigContextView -from ipam.models import IPAddress, VLANGroup +from ipam.models import ASN, IPAddress, VLANGroup from ipam.tables import InterfaceVLANTable from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.views import generic @@ -400,7 +400,9 @@ def get_extra_context(self, request, instance): scope_type=ContentType.objects.get_for_model(Site), scope_id=instance.pk ), 'site'), - (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'), + (ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'), + (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), + 'site_id'), ), ), } diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 4b12fec54e2..7a542764faf 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -269,7 +269,14 @@ class ASNView(GetRelatedModelsMixin, generic.ObjectView): def get_extra_context(self, request, instance): return { - 'related_models': self.get_related_models(request, [instance]), + 'related_models': self.get_related_models( + request, + instance, + extra=( + (Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), + (Provider.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), + ), + ), } From 9b5ca90c27813e55b53310ff1126d7613cf5846b Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Mon, 10 Jun 2024 21:49:49 +0200 Subject: [PATCH 5/6] Add method call keywords for clarification --- netbox/dcim/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 8d6494c16ec..3b8c862a731 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -896,7 +896,7 @@ class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView): def get_extra_context(self, request, instance): return { - 'related_models': self.get_related_models(request, instance, [ + 'related_models': self.get_related_models(request, instance, omit=[ ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, @@ -1135,7 +1135,7 @@ class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView): def get_extra_context(self, request, instance): return { - 'related_models': self.get_related_models(request, instance, [ + 'related_models': self.get_related_models(request, instance, omit=[ ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, From 5277687d844110abaa4eeb464c7cc3eb453e7bcd Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Mon, 10 Jun 2024 21:53:17 +0200 Subject: [PATCH 6/6] Cleanup related models --- netbox/circuits/views.py | 3 +-- netbox/ipam/views.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index e5c057a44f2..b10b83b23dc 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -155,8 +155,7 @@ def get_extra_context(self, request, instance): 'related_models': self.get_related_models( request, instance, - [CircuitTermination], - ( + extra=( ( Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), 'provider_network_id', diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 7a542764faf..12c86c53315 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -48,7 +48,7 @@ def get_extra_context(self, request, instance): ) return { - 'related_models': self.get_related_models(request, instance), + 'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]), 'import_targets_table': import_targets_table, 'export_targets_table': export_targets_table, }