17929 Add Scope Mixins to Prefix (#17930)

* 17929 Add Scope Mixins to Prefix

* 17929 Add Scope Mixins to Prefix

* 17929 fixes for tests

* 17929 merge latest scope changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 12596 review changes

* 17929 fix migrations
This commit is contained in:
Arthur Hanson 2024-11-15 11:55:46 -08:00 committed by GitHub
parent 6ab0792f02
commit 9fe6685562
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 187 additions and 290 deletions

View File

@ -21,7 +21,7 @@ __all__ = (
class RegionSerializer(NestedGroupModelSerializer):
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True, default=0)
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
class Meta:
model = Region
@ -35,7 +35,7 @@ class RegionSerializer(NestedGroupModelSerializer):
class SiteGroupSerializer(NestedGroupModelSerializer):
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True, default=0)
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
class Meta:
model = SiteGroup
@ -63,7 +63,7 @@ class SiteSerializer(NetBoxModelSerializer):
# Related object counts
circuit_count = RelatedObjectCountField('circuit_terminations')
device_count = RelatedObjectCountField('devices')
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
rack_count = RelatedObjectCountField('racks')
vlan_count = RelatedObjectCountField('vlans')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
@ -86,7 +86,7 @@ class LocationSerializer(NestedGroupModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True, default=0)
device_count = serializers.IntegerField(read_only=True, default=0)
prefix_count = RelatedObjectCountField('_prefixes')
prefix_count = RelatedObjectCountField('prefix_set')
class Meta:
model = Location

View File

@ -0,0 +1,67 @@
import django_filters
from django.utils.translation import gettext as _
from netbox.filtersets import BaseFilterSet
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
from .models import *
__all__ = (
'ScopedFilterSet',
)
class ScopedFilterSet(BaseFilterSet):
"""
Provides additional filtering functionality for location, site, etc.. for Scoped models.
"""
scope_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)

View File

@ -73,7 +73,6 @@ __all__ = (
'RearPortFilterSet',
'RearPortTemplateFilterSet',
'RegionFilterSet',
'ScopedFilterSet',
'SiteFilterSet',
'SiteGroupFilterSet',
'VirtualChassisFilterSet',
@ -2345,60 +2344,3 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet):
class Meta:
model = Interface
fields = tuple()
class ScopedFilterSet(BaseFilterSet):
"""
Provides additional filtering functionality for location, site, etc.. for Scoped models.
"""
scope_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_site_group',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)

View File

@ -461,7 +461,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
@ -707,7 +707,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
@ -739,7 +739,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
@ -763,7 +763,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
@strawberry_django.field
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
return self._clusters.all()
return self.cluster_set.all()
@strawberry_django.field
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:

View File

@ -59,28 +59,24 @@ class CachedScopeMixin(models.Model):
_location = models.ForeignKey(
to='dcim.Location',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)
_site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)
_region = models.ForeignKey(
to='dcim.Region',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)
_site_group = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.CASCADE,
related_name='_%(class)ss',
blank=True,
null=True
)

View File

@ -2,8 +2,9 @@ from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.constants import LOCATION_SCOPE_TYPES
from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, PREFIX_SCOPE_TYPES
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
from ipam.models import Aggregate, IPAddress, IPRange, Prefix
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
@ -47,7 +48,7 @@ class PrefixSerializer(NetBoxModelSerializer):
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=PREFIX_SCOPE_TYPES
model__in=LOCATION_SCOPE_TYPES
),
allow_null=True,
required=False,

View File

@ -18,7 +18,7 @@ class IPAMConfig(AppConfig):
# Register denormalized fields
denormalized.register(Prefix, '_site', {
'_region': 'region',
'_sitegroup': 'group',
'_site_group': 'group',
})
denormalized.register(Prefix, '_location', {
'_site': 'site',

View File

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

View File

@ -1,5 +1,6 @@
import django_filters
import netaddr
from dcim.base_filtersets import ScopedFilterSet
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Q
@ -9,7 +10,7 @@ from drf_spectacular.utils import extend_schema_field
from netaddr.core import AddrFormatError
from circuits.models import Provider
from dcim.models import Device, Interface, Location, Region, Site, SiteGroup
from dcim.models import Device, Interface, Region, Site, SiteGroup
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import (
@ -273,7 +274,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
fields = ('id', 'name', 'slug', 'description', 'weight')
class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
family = django_filters.NumberFilter(
field_name='prefix',
lookup_expr='family'
@ -334,57 +335,6 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='rd',
label=_('VRF (RD)'),
)
scope_type = ContentTypeFilter()
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
label=_('Region (ID)'),
)
region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='_region',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_sitegroup',
lookup_expr='in',
label=_('Site group (ID)'),
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='_sitegroup',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
field_name='_site',
label=_('Site (ID)'),
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='_site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label=_('Site (slug)'),
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
label=_('Location (ID)'),
)
location = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='_location',
lookup_expr='in',
to_field_name='slug',
label=_('Location (slug)'),
)
vlan_id = django_filters.ModelMultipleChoiceFilter(
queryset=VLAN.objects.all(),
label=_('VLAN (ID)'),

View File

@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from dcim.forms.mixins import ScopedBulkEditForm
from dcim.models import Region, Site, SiteGroup
from ipam.choices import *
from ipam.constants import *
@ -205,20 +206,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('description',)
class PrefixBulkEditForm(NetBoxModelBulkEditForm):
scope_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
required=False,
label=_('Scope type')
)
scope = DynamicModelChoiceField(
label=_('Scope'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
@ -286,20 +274,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
'vlan', 'vrf', 'tenant', 'role', 'scope', 'description', 'comments',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if scope_type_id := get_field_value(self, 'scope_type'):
try:
scope_type = ContentType.objects.get(pk=scope_type_id)
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
class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
vrf = DynamicModelChoiceField(

View File

@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site
from dcim.forms.mixins import ScopedImportForm
from ipam.choices import *
from ipam.constants import *
from ipam.models import *
@ -154,7 +155,7 @@ class RoleImportForm(NetBoxModelImportForm):
fields = ('name', 'slug', 'weight', 'description', 'tags')
class PrefixImportForm(NetBoxModelImportForm):
class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(),
@ -169,11 +170,6 @@ class PrefixImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Assigned tenant')
)
scope_type = CSVContentTypeField(
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
required=False,
label=_('Scope type (app & model)')
)
vlan_group = CSVModelChoiceField(
label=_('VLAN group'),
queryset=VLANGroup.objects.all(),
@ -208,7 +204,7 @@ class PrefixImportForm(NetBoxModelImportForm):
'mark_utilized', 'description', 'comments', 'tags',
)
labels = {
'scope_id': 'Scope ID',
'scope_id': _('Scope ID'),
}
def __init__(self, data=None, *args, **kwargs):

View File

@ -4,6 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site
from dcim.forms.mixins import ScopedForm
from ipam.choices import *
from ipam.constants import *
from ipam.formfields import IPNetworkFormField
@ -197,25 +198,12 @@ class RoleForm(NetBoxModelForm):
]
class PrefixForm(TenancyForm, NetBoxModelForm):
class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label=_('VRF')
)
scope_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
widget=HTMXSelect(),
required=False,
label=_('Scope type')
)
scope = DynamicModelChoiceField(
label=_('Scope'),
queryset=Site.objects.none(), # Initial queryset
required=False,
disabled=True,
selector=True
)
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
@ -248,36 +236,6 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
'tenant', 'description', 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance is not None and instance.scope:
initial['scope'] = instance.scope
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if scope_type_id := get_field_value(self, 'scope_type'):
try:
scope_type = ContentType.objects.get(pk=scope_type_id)
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
if self.instance and scope_type_id != self.instance.scope_type_id:
self.initial['scope'] = None
def clean(self):
super().clean()
# Assign the selected scope (if any)
self.instance.scope = self.cleaned_data.get('scope')
class IPRangeForm(TenancyForm, NetBoxModelForm):
vrf = DynamicModelChoiceField(

View File

@ -154,7 +154,7 @@ class IPRangeType(NetBoxObjectType):
@strawberry_django.type(
models.Prefix,
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_sitegroup'),
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
filters=PrefixFilter
)
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):

View File

@ -11,11 +11,11 @@ def populate_denormalized_fields(apps, schema_editor):
prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site')
for prefix in prefixes:
prefix._region_id = prefix.site.region_id
prefix._sitegroup_id = prefix.site.group_id
prefix._site_group_id = prefix.site.group_id
prefix._site_id = prefix.site_id
# Note: Location cannot be set prior to migration
Prefix.objects.bulk_update(prefixes, ['_region', '_sitegroup', '_site'])
Prefix.objects.bulk_update(prefixes, ['_region', '_site_group', '_site'])
class Migration(migrations.Migration):
@ -29,22 +29,22 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='prefix',
name='_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.location'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'),
),
migrations.AddField(
model_name='prefix',
name='_region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.region'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'),
),
migrations.AddField(
model_name='prefix',
name='_site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.site'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
),
migrations.AddField(
model_name='prefix',
name='_sitegroup',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.sitegroup'),
name='_site_group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'),
),
# Populate denormalized FK values

View File

@ -1,5 +1,4 @@
import netaddr
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.db import models
@ -9,6 +8,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from core.models import ObjectType
from dcim.models.mixins import CachedScopeMixin
from ipam.choices import *
from ipam.constants import *
from ipam.fields import IPNetworkField, IPAddressField
@ -198,7 +198,7 @@ class Role(OrganizationalModel):
return self.name
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, PrimaryModel):
"""
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
@ -208,22 +208,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
verbose_name=_('prefix'),
help_text=_('IPv4 or IPv6 network with mask')
)
scope_type = models.ForeignKey(
to='contenttypes.ContentType',
on_delete=models.PROTECT,
limit_choices_to=Q(model__in=PREFIX_SCOPE_TYPES),
related_name='+',
blank=True,
null=True
)
scope_id = models.PositiveBigIntegerField(
blank=True,
null=True
)
scope = GenericForeignKey(
ct_field='scope_type',
fk_field='scope_id'
)
vrf = models.ForeignKey(
to='ipam.VRF',
on_delete=models.PROTECT,
@ -272,36 +256,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
help_text=_("Treat as fully utilized")
)
# Cached associations to enable efficient filtering
_location = models.ForeignKey(
to='dcim.Location',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
_site = models.ForeignKey(
to='dcim.Site',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
_region = models.ForeignKey(
to='dcim.Region',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
_sitegroup = models.ForeignKey(
to='dcim.SiteGroup',
on_delete=models.CASCADE,
related_name='_prefixes',
blank=True,
null=True
)
# Cached depth & child counts
_depth = models.PositiveSmallIntegerField(
default=0,
@ -368,25 +322,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
super().save(*args, **kwargs)
def cache_related_objects(self):
self._region = self._sitegroup = self._site = self._location = None
if self.scope_type:
scope_type = self.scope_type.model_class()
if scope_type == apps.get_model('dcim', 'region'):
self._region = self.scope
elif scope_type == apps.get_model('dcim', 'sitegroup'):
self._sitegroup = self.scope
elif scope_type == apps.get_model('dcim', 'site'):
self._region = self.scope.region
self._sitegroup = self.scope.group
self._site = self.scope
elif scope_type == apps.get_model('dcim', 'location'):
self._region = self.scope.site.region
self._sitegroup = self.scope.site.group
self._site = self.scope.site
self._location = self.scope
cache_related_objects.alters_data = True
@property
def family(self):
return self.prefix.version if self.prefix else None

View File

@ -129,7 +129,7 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None):
for field_name, field in serializer_class._declared_fields.items():
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
related_field = model._meta.get_field(field.relation).field
related_field = getattr(model, field.relation).field
annotations[field_name] = count_related(related_field.model, related_field.name)
return annotations

View File

@ -2,7 +2,8 @@ import django_filters
from django.db.models import Q
from django.utils.translation import gettext as _
from dcim.filtersets import CommonInterfaceFilterSet, ScopedFilterSet
from dcim.filtersets import CommonInterfaceFilterSet
from dcim.base_filtersets import ScopedFilterSet
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.9 on 2024-11-14 19:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0196_qinq_svlan'),
('virtualization', '0045_clusters_cached_relations'),
]
operations = [
migrations.AlterField(
model_name='cluster',
name='_location',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'
),
),
migrations.AlterField(
model_name='cluster',
name='_region',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'
),
),
migrations.AlterField(
model_name='cluster',
name='_site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
),
migrations.AlterField(
model_name='cluster',
name='_site_group',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'
),
),
]

View File

@ -4,7 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0045_clusters_cached_relations'),
('virtualization', '0046_alter_cluster__location_alter_cluster__region_and_more'),
('dcim', '0197_natural_sort_collation'),
]

View File

@ -2,7 +2,7 @@ import django_filters
from django.db.models import Q
from dcim.choices import LinkStatusChoices
from dcim.filtersets import ScopedFilterSet
from dcim.base_filtersets import ScopedFilterSet
from dcim.models import Interface
from ipam.models import VLAN
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.9 on 2024-11-14 19:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0196_qinq_svlan'),
('wireless', '0011_wirelesslan__location_wirelesslan__region_and_more'),
]
operations = [
migrations.AlterField(
model_name='wirelesslan',
name='_location',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'
),
),
migrations.AlterField(
model_name='wirelesslan',
name='_region',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'
),
),
migrations.AlterField(
model_name='wirelesslan',
name='_site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
),
migrations.AlterField(
model_name='wirelesslan',
name='_site_group',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'
),
),
]

View File

@ -4,7 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wireless', '0011_wirelesslan__location_wirelesslan__region_and_more'),
('wireless', '0012_alter_wirelesslan__location_and_more'),
('dcim', '0197_natural_sort_collation'),
]