#6414: Add FKs for region, site group, and location on Prefix

This commit is contained in:
Jeremy Stretch 2024-10-11 15:48:13 -04:00
parent 727de0fb59
commit 5dd92ff048
13 changed files with 209 additions and 46 deletions

View File

@ -2,9 +2,9 @@ from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from dcim.api.serializers_.sites import SiteSerializer from ipam.api.field_serializers import IPAddressField, IPNetworkField
from ipam.choices import * from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, PREFIX_SCOPE_TYPES
from ipam.models import Aggregate, IPAddress, IPRange, Prefix from ipam.models import Aggregate, IPAddress, IPRange, Prefix
from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
@ -15,7 +15,6 @@ from .nested import NestedIPAddressSerializer
from .roles import RoleSerializer from .roles import RoleSerializer
from .vlans import VLANSerializer from .vlans import VLANSerializer
from .vrfs import VRFSerializer from .vrfs import VRFSerializer
from ..field_serializers import IPAddressField, IPNetworkField
__all__ = ( __all__ = (
'AggregateSerializer', 'AggregateSerializer',
@ -45,7 +44,16 @@ class AggregateSerializer(NetBoxModelSerializer):
class PrefixSerializer(NetBoxModelSerializer): class PrefixSerializer(NetBoxModelSerializer):
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
site = SiteSerializer(nested=True, required=False, allow_null=True) scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=PREFIX_SCOPE_TYPES
),
allow_null=True,
required=False,
default=None
)
# TODO: Handle writing to scope
scope = serializers.SerializerMethodField(read_only=True)
vrf = VRFSerializer(nested=True, required=False, allow_null=True) vrf = VRFSerializer(nested=True, required=False, allow_null=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
vlan = VLANSerializer(nested=True, required=False, allow_null=True) vlan = VLANSerializer(nested=True, required=False, allow_null=True)
@ -58,12 +66,20 @@ class PrefixSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Prefix model = Prefix
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'id', 'url', 'display_url', 'display', 'family', 'prefix', 'site', 'vrf', 'scope_type', 'scope', 'tenant',
'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'children', '_depth', 'created', 'last_updated', 'children', '_depth',
] ]
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_scope(self, obj):
if obj.scope is None:
return None
serializer = get_serializer_for_model(obj.scope)
context = {'request': self.context['request']}
return serializer(obj.scope, nested=True, context=context).data
class PrefixLengthSerializer(serializers.Serializer): class PrefixLengthSerializer(serializers.Serializer):

View File

@ -79,7 +79,7 @@ class RoleViewSet(NetBoxModelViewSet):
class PrefixViewSet(NetBoxModelViewSet): class PrefixViewSet(NetBoxModelViewSet):
queryset = Prefix.objects.all() queryset = Prefix.objects.prefetch_related('region', 'site_group', 'site', 'location')
serializer_class = serializers.PrefixSerializer serializer_class = serializers.PrefixSerializer
filterset_class = filtersets.PrefixFilterSet filterset_class = filtersets.PrefixFilterSet

View File

@ -23,6 +23,11 @@ VRF_RD_MAX_LENGTH = 21
PREFIX_LENGTH_MIN = 1 PREFIX_LENGTH_MIN = 1
PREFIX_LENGTH_MAX = 127 # IPv6 PREFIX_LENGTH_MAX = 127 # IPv6
# models values for ContentTypes which may be Prefix scope types
PREFIX_SCOPE_TYPES = (
'region', 'sitegroup', 'site', 'location',
)
# #
# IPAddresses # IPAddresses

View File

@ -9,7 +9,7 @@ from drf_spectacular.utils import extend_schema_field
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from circuits.models import Provider from circuits.models import Provider
from dcim.models import Device, Interface, Region, Site, SiteGroup from dcim.models import Device, Interface, Location, Region, Site, SiteGroup
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import ( from utilities.filters import (
@ -332,6 +332,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='rd', to_field_name='rd',
label=_('VRF (RD)'), label=_('VRF (RD)'),
) )
# TODO: Figure out region & site filters
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -368,6 +369,17 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='slug', to_field_name='slug',
label=_('Site (slug)'), label=_('Site (slug)'),
) )
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)
vlan_id = django_filters.ModelMultipleChoiceFilter( vlan_id = django_filters.ModelMultipleChoiceFilter(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
label=_('VLAN (ID)'), label=_('VLAN (ID)'),

View File

@ -201,12 +201,18 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
required=False, required=False,
label=_('VRF') label=_('VRF')
) )
site = DynamicModelChoiceField( scope_type = ContentTypeChoiceField(
label=_('Site'), queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
queryset=Site.objects.all(), widget=HTMXSelect(),
required=False, required=False,
selector=True, label=_('Scope type')
null_option='None' )
scope = DynamicModelChoiceField(
label=_('Scope'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
) )
vlan = DynamicModelChoiceField( vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
@ -228,7 +234,8 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
FieldSet( FieldSet(
'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix') 'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
), ),
FieldSet('site', 'vlan', name=_('Site/VLAN Assignment')), FieldSet('scope_type', 'scope', name=_('Scope')),
FieldSet('vlan', name=_('VLAN Assignment')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
) )
@ -239,6 +246,32 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
'description', 'comments', 'tags', 'description', 'comments', 'tags',
] ]
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance is not None and instance.scope and 'scope_type' not in initial:
initial['scope_type'] = instance.scope_type.pk
initial['scope'] = instance.scope
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if scope_type := get_field_value(self, 'scope_type'):
try:
scope_type = ContentType.objects.get(pk=scope_type)
model = scope_type.model_class()
self.fields['scope'].queryset = model.objects.all()
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
self.fields['scope'].disabled = False
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
except ObjectDoesNotExist:
pass
def save(self, *args, **kwargs):
self.instance.scope = self.cleaned_data['scope']
return super().save(*args, **kwargs)
class IPRangeForm(TenancyForm, NetBoxModelForm): class IPRangeForm(TenancyForm, NetBoxModelForm):
vrf = DynamicModelChoiceField( vrf = DynamicModelChoiceField(

View File

@ -163,6 +163,15 @@ class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
@strawberry_django.field
def scope(self) -> Annotated[Union[
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
], strawberry.union("PrefixScopeType")] | None:
return self.scope
@strawberry_django.type( @strawberry_django.type(
models.RIR, models.RIR,

View File

@ -0,0 +1,28 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0193_poweroutlet_color'),
('ipam', '0070_vlangroup_vlan_id_ranges'),
]
operations = [
migrations.AddField(
model_name='prefix',
name='location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='dcim.location'),
),
migrations.AddField(
model_name='prefix',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='dcim.region'),
),
migrations.AddField(
model_name='prefix',
name='site_group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='prefixes', to='dcim.sitegroup'),
),
]

View File

@ -1,4 +1,5 @@
import netaddr import netaddr
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
@ -207,6 +208,20 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
verbose_name=_('prefix'), verbose_name=_('prefix'),
help_text=_('IPv4 or IPv6 network with mask') help_text=_('IPv4 or IPv6 network with mask')
) )
region = models.ForeignKey(
to='dcim.Region',
on_delete=models.PROTECT,
related_name='prefixes',
blank=True,
null=True
)
site_group = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.PROTECT,
related_name='prefixes',
blank=True,
null=True
)
site = models.ForeignKey( site = models.ForeignKey(
to='dcim.Site', to='dcim.Site',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -214,6 +229,13 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
blank=True, blank=True,
null=True null=True
) )
location = models.ForeignKey(
to='dcim.Location',
on_delete=models.PROTECT,
related_name='prefixes',
blank=True,
null=True
)
vrf = models.ForeignKey( vrf = models.ForeignKey(
to='ipam.VRF', to='ipam.VRF',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -275,7 +297,8 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
objects = PrefixQuerySet.as_manager() objects = PrefixQuerySet.as_manager()
clone_fields = ( clone_fields = (
'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
# 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'scope_type', 'scope',
) )
class Meta: class Meta:
@ -341,6 +364,27 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
def children(self): def children(self):
return self._children return self._children
@property
def scope_type(self):
if not self.scope:
return None
return ObjectType.objects.get_for_model(self.scope)
@property
def scope(self):
return self.region or self.site_group or self.site or self.location
@scope.setter
def scope(self, value):
self.region = self.site_group = self.site = self.location = None
if value is not None:
if value._meta.model_name == 'sitegroup':
# TODO: Fix this hack
field_name = 'site_group'
else:
field_name = value._meta.model_name
setattr(self, field_name, value)
def _set_prefix_length(self, value): def _set_prefix_length(self, value):
""" """
Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly, Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,

View File

@ -241,10 +241,30 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
template_code=VRF_LINK, template_code=VRF_LINK,
verbose_name=_('VRF') verbose_name=_('VRF')
) )
scope_type = columns.ContentTypeColumn(
verbose_name=_('Scope Type'),
)
scope = tables.Column(
linkify=True,
orderable=False,
verbose_name=_('Scope')
)
region = tables.Column(
verbose_name=_('Region'),
linkify=True
)
site_group = tables.Column(
verbose_name=_('Site Group'),
linkify=True
)
site = tables.Column( site = tables.Column(
verbose_name=_('Site'), verbose_name=_('Site'),
linkify=True linkify=True
) )
location = tables.Column(
verbose_name=_('Location'),
linkify=True
)
vlan_group = tables.Column( vlan_group = tables.Column(
accessor='vlan__group', accessor='vlan__group',
linkify=True, linkify=True,
@ -285,11 +305,11 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
model = Prefix model = Prefix
fields = ( fields = (
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', 'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
'site', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags', 'region', 'site_group', 'site', 'location', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized',
'created', 'last_updated', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'vlan', 'role', 'description',
) )
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not record.pk else '', 'class': lambda record: 'success' if not record.pk else '',

View File

@ -656,14 +656,14 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants) Tenant.objects.bulk_create(tenants)
prefixes = ( prefixes = (
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True, description='foobar1'), Prefix(prefix='10.0.0.0/24', tenant=None, scope=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True, description='foobar1'),
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0], description='foobar2'), Prefix(prefix='10.0.1.0/24', tenant=tenants[0], scope=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0], description='foobar2'),
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='10.0.2.0/24', tenant=tenants[1], scope=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), Prefix(prefix='10.0.3.0/24', tenant=tenants[2], scope=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True), Prefix(prefix='2001:db8::/64', tenant=None, scope=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]), Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], scope=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED), Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], scope=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED), Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], scope=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
Prefix(prefix='10.0.0.0/16'), Prefix(prefix='10.0.0.0/16'),
Prefix(prefix='2001:db8::/32'), Prefix(prefix='2001:db8::/32'),
) )

View File

@ -409,9 +409,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Role.objects.bulk_create(roles) Role.objects.bulk_create(roles)
prefixes = ( prefixes = (
Prefix(prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), Prefix(prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]),
Prefix(prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), Prefix(prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]),
Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]),
) )
Prefix.objects.bulk_create(prefixes) Prefix.objects.bulk_create(prefixes)

View File

@ -352,7 +352,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
def get_children(self, request, parent): def get_children(self, request, parent):
return Prefix.objects.restrict(request.user, 'view').filter( return Prefix.objects.restrict(request.user, 'view').filter(
prefix__net_contained_or_equal=str(parent.prefix) prefix__net_contained_or_equal=str(parent.prefix)
).prefetch_related('site', 'role', 'tenant', 'tenant__group', 'vlan') ).prefetch_related('region', 'site_group', 'site', 'location', 'role', 'tenant', 'tenant__group', 'vlan')
def prep_table_data(self, request, queryset, parent): def prep_table_data(self, request, queryset, parent):
# Determine whether to show assigned prefixes, available prefixes, or both # Determine whether to show assigned prefixes, available prefixes, or both
@ -467,7 +467,7 @@ class RoleBulkDeleteView(generic.BulkDeleteView):
# #
class PrefixListView(generic.ObjectListView): class PrefixListView(generic.ObjectListView):
queryset = Prefix.objects.all() queryset = Prefix.objects.prefetch_related('region', 'site_group', 'site', 'location')
filterset = filtersets.PrefixFilterSet filterset = filtersets.PrefixFilterSet
filterset_form = forms.PrefixFilterForm filterset_form = forms.PrefixFilterForm
table = tables.PrefixTable table = tables.PrefixTable
@ -492,7 +492,7 @@ class PrefixView(generic.ObjectView):
).filter( ).filter(
prefix__net_contains=str(instance.prefix) prefix__net_contains=str(instance.prefix)
).prefetch_related( ).prefetch_related(
'site', 'role', 'tenant', 'vlan', 'region', 'site_group', 'site', 'location', 'role', 'tenant', 'vlan',
) )
parent_prefix_table = tables.PrefixTable( parent_prefix_table = tables.PrefixTable(
list(parent_prefixes), list(parent_prefixes),
@ -506,7 +506,7 @@ class PrefixView(generic.ObjectView):
).exclude( ).exclude(
pk=instance.pk pk=instance.pk
).prefetch_related( ).prefetch_related(
'site', 'role', 'tenant', 'vlan', 'region', 'site_group', 'site', 'location', 'role', 'tenant', 'vlan',
) )
duplicate_prefix_table = tables.PrefixTable( duplicate_prefix_table = tables.PrefixTable(
list(duplicate_prefixes), list(duplicate_prefixes),
@ -538,7 +538,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
def get_children(self, request, parent): def get_children(self, request, parent):
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related( return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
'site', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group' 'region', 'site_group', 'site', 'location', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
) )
def prep_table_data(self, request, queryset, parent): def prep_table_data(self, request, queryset, parent):
@ -631,14 +631,14 @@ class PrefixBulkImportView(generic.BulkImportView):
class PrefixBulkEditView(generic.BulkEditView): class PrefixBulkEditView(generic.BulkEditView):
queryset = Prefix.objects.prefetch_related('vrf__tenant') queryset = Prefix.objects.prefetch_related('region', 'site_group', 'site', 'location')
filterset = filtersets.PrefixFilterSet filterset = filtersets.PrefixFilterSet
table = tables.PrefixTable table = tables.PrefixTable
form = forms.PrefixBulkEditForm form = forms.PrefixBulkEditForm
class PrefixBulkDeleteView(generic.BulkDeleteView): class PrefixBulkDeleteView(generic.BulkDeleteView):
queryset = Prefix.objects.prefetch_related('vrf__tenant') queryset = Prefix.objects.prefetch_related('region', 'site_group', 'site', 'location')
filterset = filtersets.PrefixFilterSet filterset = filtersets.PrefixFilterSet
table = tables.PrefixTable table = tables.PrefixTable

View File

@ -44,17 +44,13 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if object.site.region %}
<tr>
<th scope="row">{% trans "Region" %}</th>
<td>
{% nested_tree object.site.region %}
</td>
</tr>
{% endif %}
<tr> <tr>
<th scope="row">{% trans "Site" %}</th> <th scope="row">{% trans "Scope" %}</th>
<td>{{ object.site|linkify|placeholder }}</td> {% if object.scope %}
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
{% else %}
<td>{{ ''|placeholder }}</td>
{% endif %}
</tr> </tr>
<tr> <tr>
<th scope="row">{% trans "VLAN" %}</th> <th scope="row">{% trans "VLAN" %}</th>