15794 Make "related objects" dynamic (#15876)

* Closes #15794: Make "related objects" dynamic

Instead of hardcoding relationships between models for the detail view,
they are now dynamically generated.

* Fix related models call

* 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.

* Fix relations view for ASNs

ASNs have ManyToMany relationships and therefore can't used automatic
resolving. Explicit relations have been restored as before.

* Add method call keywords for clarification

* Cleanup related models

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
Alexander Haase 2024-06-12 15:46:41 +02:00 committed by GitHub
parent 763d65bed9
commit 5353f83710
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 176 additions and 238 deletions

View File

@ -7,7 +7,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.query import count_related from utilities.query import count_related
from utilities.views import register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -26,17 +26,12 @@ class ProviderListView(generic.ObjectListView):
@register_model_view(Provider) @register_model_view(Provider)
class ProviderView(generic.ObjectView): class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
def get_extra_context(self, request, instance): 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 { 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) @register_model_view(ProviderAccount)
class ProviderAccountView(generic.ObjectView): class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderAccount.objects.all() queryset = ProviderAccount.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -156,19 +147,21 @@ class ProviderNetworkListView(generic.ObjectListView):
@register_model_view(ProviderNetwork) @register_model_view(ProviderNetwork)
class ProviderNetworkView(generic.ObjectView): class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ProviderNetwork.objects.all() queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = ( return {
'related_models': self.get_related_models(
request,
instance,
extra=(
( (
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance), Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
'provider_network_id', 'provider_network_id',
), ),
) ),
),
return {
'related_models': related_models,
} }
@ -215,16 +208,12 @@ class CircuitTypeListView(generic.ObjectListView):
@register_model_view(CircuitType) @register_model_view(CircuitType)
class CircuitTypeView(generic.ObjectView): class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Circuit.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -32,7 +32,7 @@ from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.htmx import htmx_partial from utilities.htmx import htmx_partial
from utilities.query import count_related from utilities.query 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 . import filtersets, forms, tables
from .models import * from .models import *
@ -51,16 +51,12 @@ class DataSourceListView(generic.ObjectListView):
@register_model_view(DataSource) @register_model_view(DataSource)
class DataSourceView(generic.ObjectView): class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DataSource.objects.all() queryset = DataSource.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -17,7 +17,7 @@ from jinja2.exceptions import TemplateError
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from extras.views import ObjectConfigContextView from extras.views import ObjectConfigContextView
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup from ipam.models import ASN, IPAddress, VLANGroup
from ipam.tables import InterfaceVLANTable from ipam.tables import InterfaceVLANTable
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic from netbox.views import generic
@ -27,7 +27,9 @@ from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.query import count_related from utilities.query import count_related
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.views import GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view from utilities.views import (
GetRelatedModelsMixin, GetReturnURLMixin, ObjectPermissionRequiredMixin, ViewTab, register_model_view
)
from virtualization.filtersets import VirtualMachineFilterSet from virtualization.filtersets import VirtualMachineFilterSet
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable from virtualization.tables import VirtualMachineTable
@ -226,19 +228,21 @@ class RegionListView(generic.ObjectListView):
@register_model_view(Region) @register_model_view(Region)
class RegionView(generic.ObjectView): class RegionView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Region.objects.all() queryset = Region.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
regions = instance.get_descendants(include_self=True) regions = instance.get_descendants(include_self=True)
related_models = (
(Site.objects.restrict(request.user, 'view').filter(region__in=regions), 'region_id'),
(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'),
)
return { return {
'related_models': related_models, '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'),
),
),
} }
@ -306,19 +310,21 @@ class SiteGroupListView(generic.ObjectListView):
@register_model_view(SiteGroup) @register_model_view(SiteGroup)
class SiteGroupView(generic.ObjectView): class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = SiteGroup.objects.all() queryset = SiteGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(Site.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
(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'),
)
return { return {
'related_models': related_models, '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'),
),
),
} }
@ -380,31 +386,25 @@ class SiteListView(generic.ObjectListView):
@register_model_view(Site) @register_model_view(Site)
class SiteView(generic.ObjectView): class SiteView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Site.objects.prefetch_related('tenant__group') queryset = Site.objects.prefetch_related('tenant__group')
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = ( return {
# DCIM 'related_models': self.get_related_models(
(Location.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), request,
(Rack.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), instance,
(Device.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), [CableTermination, CircuitTermination],
# 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'),
(VLANGroup.objects.restrict(request.user, 'view').filter( (VLANGroup.objects.restrict(request.user, 'view').filter(
scope_type=ContentType.objects.get_for_model(Site), scope_type=ContentType.objects.get_for_model(Site),
scope_id=instance.pk scope_id=instance.pk
), 'site'), ), 'site'),
(VLAN.objects.restrict(request.user, 'view').filter(site=instance), 'site_id'), (ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
# Circuits (Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(),
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(), 'site_id'), 'site_id'),
) ),
),
return {
'related_models': related_models,
} }
@ -466,18 +466,13 @@ class LocationListView(generic.ObjectListView):
@register_model_view(Location) @register_model_view(Location)
class LocationView(generic.ObjectView): class LocationView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Location.objects.all() queryset = Location.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
locations = instance.get_descendants(include_self=True) 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 { return {
'related_models': related_models, 'related_models': self.get_related_models(request, locations, [CableTermination]),
} }
@ -541,16 +536,12 @@ class RackRoleListView(generic.ObjectListView):
@register_model_view(RackRole) @register_model_view(RackRole)
class RackRoleView(generic.ObjectView): class RackRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Rack.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -655,15 +646,10 @@ class RackElevationListView(generic.ObjectListView):
@register_model_view(Rack) @register_model_view(Rack)
class RackView(generic.ObjectView): class RackView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role') queryset = Rack.objects.prefetch_related('site__region', 'tenant__group', 'location', 'role')
def get_extra_context(self, request, instance): 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) peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
if instance.location: if instance.location:
@ -679,7 +665,7 @@ class RackView(generic.ObjectView):
]) ])
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, [CableTermination]),
'next_rack': next_rack, 'next_rack': next_rack,
'prev_rack': prev_rack, 'prev_rack': prev_rack,
'svg_extra': svg_extra, 'svg_extra': svg_extra,
@ -838,19 +824,12 @@ class ManufacturerListView(generic.ObjectListView):
@register_model_view(Manufacturer) @register_model_view(Manufacturer)
class ManufacturerView(generic.ObjectView): class ManufacturerView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Manufacturer.objects.all() queryset = Manufacturer.objects.all()
def get_extra_context(self, request, instance): 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 { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, [InventoryItemTemplate]),
} }
@ -912,16 +891,16 @@ class DeviceTypeListView(generic.ObjectListView):
@register_model_view(DeviceType) @register_model_view(DeviceType)
class DeviceTypeView(generic.ObjectView): class DeviceTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Device.objects.restrict(request.user).filter(device_type=instance), 'device_type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
]),
} }
@ -1151,16 +1130,16 @@ class ModuleTypeListView(generic.ObjectListView):
@register_model_view(ModuleType) @register_model_view(ModuleType)
class ModuleTypeView(generic.ObjectView): class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ModuleType.objects.all() queryset = ModuleType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Module.objects.restrict(request.user).filter(module_type=instance), 'module_type_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, omit=[
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate,
InventoryItemTemplate, InterfaceTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate,
RearPortTemplate,
]),
} }
@ -1711,17 +1690,12 @@ class DeviceRoleListView(generic.ObjectListView):
@register_model_view(DeviceRole) @register_model_view(DeviceRole)
class DeviceRoleView(generic.ObjectView): class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
def get_extra_context(self, request, instance): 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 { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -1775,17 +1749,12 @@ class PlatformListView(generic.ObjectListView):
@register_model_view(Platform) @register_model_view(Platform)
class PlatformView(generic.ObjectView): class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Platform.objects.all() queryset = Platform.objects.all()
def get_extra_context(self, request, instance): 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 { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -2157,22 +2126,12 @@ class ModuleListView(generic.ObjectListView):
@register_model_view(Module) @register_model_view(Module)
class ModuleView(generic.ObjectView): class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Module.objects.all() queryset = Module.objects.all()
def get_extra_context(self, request, instance): 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 { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -3552,16 +3511,12 @@ class PowerPanelListView(generic.ObjectListView):
@register_model_view(PowerPanel) @register_model_view(PowerPanel)
class PowerPanelView(generic.ObjectView): class PowerPanelView(GetRelatedModelsMixin, generic.ObjectView):
queryset = PowerPanel.objects.all() queryset = PowerPanel.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(PowerFeed.objects.restrict(request.user).filter(power_panel=instance), 'power_panel_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -3665,16 +3620,18 @@ class VirtualDeviceContextListView(generic.ObjectListView):
@register_model_view(VirtualDeviceContext) @register_model_view(VirtualDeviceContext)
class VirtualDeviceContextView(generic.ObjectView): class VirtualDeviceContextView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VirtualDeviceContext.objects.all() queryset = VirtualDeviceContext.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(
request,
instance,
extra=(
(Interface.objects.restrict(request.user, 'view').filter(vdcs__in=[instance]), 'vdc_id'),
),
),
} }

View File

@ -12,7 +12,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.query import count_related from utilities.query import count_related
from utilities.tables import get_table_ordering from utilities.tables import get_table_ordering
from utilities.views import ViewTab, register_model_view from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface from virtualization.models import VMInterface
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -34,15 +34,10 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF) @register_model_view(VRF)
class VRFView(generic.ObjectView): class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all() queryset = VRF.objects.all()
def get_extra_context(self, request, instance): 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( import_targets_table = tables.RouteTargetTable(
instance.import_targets.all(), instance.import_targets.all(),
orderable=False orderable=False
@ -53,7 +48,7 @@ class VRFView(generic.ObjectView):
) )
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]),
'import_targets_table': import_targets_table, 'import_targets_table': import_targets_table,
'export_targets_table': export_targets_table, 'export_targets_table': export_targets_table,
} }
@ -147,16 +142,12 @@ class RIRListView(generic.ObjectListView):
@register_model_view(RIR) @register_model_view(RIR)
class RIRView(generic.ObjectView): class RIRView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RIR.objects.all() queryset = RIR.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Aggregate.objects.restrict(request.user, 'view').filter(rir=instance), 'rir_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -273,17 +264,19 @@ class ASNListView(generic.ObjectListView):
@register_model_view(ASN) @register_model_view(ASN)
class ASNView(generic.ObjectView): class ASNView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ASN.objects.all() queryset = ASN.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = ( return {
'related_models': self.get_related_models(
request,
instance,
extra=(
(Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'), (Site.objects.restrict(request.user, 'view').filter(asns__in=[instance]), 'asn_id'),
(Provider.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,
} }
@ -427,18 +420,12 @@ class RoleListView(generic.ObjectListView):
@register_model_view(Role) @register_model_view(Role)
class RoleView(generic.ObjectView): class RoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Role.objects.all() queryset = Role.objects.all()
def get_extra_context(self, request, instance): 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 { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -926,16 +913,12 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup) @register_model_view(VLANGroup)
class VLANGroupView(generic.ObjectView): class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(VLAN.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -4,8 +4,7 @@ from django.utils.translation import gettext as _
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.relations import get_related_models from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from utilities.views import register_model_view, ViewTab
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -56,17 +55,14 @@ class TenantGroupListView(generic.ObjectListView):
@register_model_view(TenantGroup) @register_model_view(TenantGroup)
class TenantGroupView(generic.ObjectView): class TenantGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = TenantGroup.objects.all() queryset = TenantGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(Tenant.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, groups),
} }
@ -123,17 +119,12 @@ class TenantListView(generic.ObjectListView):
@register_model_view(Tenant) @register_model_view(Tenant)
class TenantView(generic.ObjectView): class TenantView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Tenant.objects.all() queryset = Tenant.objects.all()
def get_extra_context(self, request, instance): 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 { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }
@ -189,17 +180,14 @@ class ContactGroupListView(generic.ObjectListView):
@register_model_view(ContactGroup) @register_model_view(ContactGroup)
class ContactGroupView(generic.ObjectView): class ContactGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ContactGroup.objects.all() queryset = ContactGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(Contact.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, groups),
} }
@ -256,16 +244,12 @@ class ContactRoleListView(generic.ObjectListView):
@register_model_view(ContactRole) @register_model_view(ContactRole)
class ContactRoleView(generic.ObjectView): class ContactRoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ContactRole.objects.all() queryset = ContactRole.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(ContactAssignment.objects.restrict(request.user, 'view').filter(role=instance), 'role_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -1,3 +1,5 @@
from typing import Iterable
from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse from django.urls import reverse
@ -6,10 +8,12 @@ from django.utils.translation import gettext_lazy as _
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from netbox.registry import registry from netbox.registry import registry
from utilities.relations import get_related_models
from .permissions import resolve_permission from .permissions import resolve_permission
__all__ = ( __all__ = (
'ContentTypePermissionRequiredMixin', 'ContentTypePermissionRequiredMixin',
'GetRelatedModelsMixin',
'GetReturnURLMixin', 'GetReturnURLMixin',
'ObjectPermissionRequiredMixin', 'ObjectPermissionRequiredMixin',
'ViewTab', 'ViewTab',
@ -142,6 +146,46 @@ class GetReturnURLMixin:
return reverse('home') return reverse('home')
class GetRelatedModelsMixin:
"""
Provides logic for collecting all related models for the currently viewed model.
"""
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.
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.
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(
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(extra)
return sorted(related_models, key=lambda x: x[0].model._meta.verbose_name.lower())
class ViewTab: class ViewTab:
""" """
ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for

View File

@ -20,7 +20,7 @@ from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.query import count_related from utilities.query import count_related
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.views import ViewTab, register_model_view from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -39,16 +39,12 @@ class ClusterTypeListView(generic.ObjectListView):
@register_model_view(ClusterType) @register_model_view(ClusterType)
class ClusterTypeView(generic.ObjectView): class ClusterTypeView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ClusterType.objects.all() queryset = ClusterType.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Cluster.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
)
return { 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) @register_model_view(ClusterGroup)
class ClusterGroupView(generic.ObjectView): class ClusterGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ClusterGroup.objects.all() queryset = ClusterGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Cluster.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -2,7 +2,7 @@ from ipam.tables import RouteTargetTable
from netbox.views import generic from netbox.views import generic
from tenancy.views import ObjectContactsView from tenancy.views import ObjectContactsView
from utilities.query import count_related from utilities.query import count_related
from utilities.views import register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -21,16 +21,12 @@ class TunnelGroupListView(generic.ObjectListView):
@register_model_view(TunnelGroup) @register_model_view(TunnelGroup)
class TunnelGroupView(generic.ObjectView): class TunnelGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = TunnelGroup.objects.all() queryset = TunnelGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = (
(Tunnel.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, instance),
} }

View File

@ -1,7 +1,7 @@
from dcim.models import Interface from dcim.models import Interface
from netbox.views import generic from netbox.views import generic
from utilities.query import count_related from utilities.query import count_related
from utilities.views import register_model_view from utilities.views import GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
@ -24,17 +24,14 @@ class WirelessLANGroupListView(generic.ObjectListView):
@register_model_view(WirelessLANGroup) @register_model_view(WirelessLANGroup)
class WirelessLANGroupView(generic.ObjectView): class WirelessLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = WirelessLANGroup.objects.all() queryset = WirelessLANGroup.objects.all()
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
groups = instance.get_descendants(include_self=True) groups = instance.get_descendants(include_self=True)
related_models = (
(WirelessLAN.objects.restrict(request.user, 'view').filter(group__in=groups), 'group_id'),
)
return { return {
'related_models': related_models, 'related_models': self.get_related_models(request, groups),
} }