Просмотр исходного кода

feat(ipam): Expose total_vlan_ids on VLAN groups

Rename the internal `_total_vlan_ids` field to `total_vlan_ids` on
VLANGroup and expose it as a read-only integer field.
This change includes a migration to rename the database column,
adds `total_vlan_ids` to VLANGroup API representations as a read-only
attribute, updates the UI table to include a "Total VLAN IDs" column,
and adjusts related tests accordingly.

Fixes #20698
Martin Hauser 5 дней назад
Родитель
Сommit
050ee5d254

+ 3 - 1
netbox/ipam/api/serializers_/vlans.py

@@ -36,6 +36,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
     scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
     scope = GFKSerializerField(read_only=True)
     vid_ranges = IntegerRangeSerializer(many=True, required=False)
+    total_vlan_ids = serializers.IntegerField(read_only=True)
     utilization = serializers.CharField(read_only=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
 
@@ -46,7 +47,8 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
         model = VLANGroup
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'vid_ranges',
-            'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'total_vlan_ids', 'tenant', 'description', 'owner', 'comments', 'tags', 'custom_fields',
+            'created', 'last_updated',
             'vlan_count', 'utilization',
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')

+ 1 - 1
netbox/ipam/filtersets.py

@@ -962,7 +962,7 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
 
     class Meta:
         model = VLANGroup
-        fields = ('id', 'name', 'slug', 'description', 'scope_id')
+        fields = ('id', 'name', 'slug', 'description', 'scope_id', 'total_vlan_ids')
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 2 - 1
netbox/ipam/graphql/filters.py

@@ -7,7 +7,7 @@ import strawberry_django
 from django.db.models import Q
 from netaddr.core import AddrFormatError
 from strawberry.scalars import ID
-from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup, StrFilterLookup
+from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, DateFilterLookup, FilterLookup, StrFilterLookup
 
 from dcim.graphql.filter_mixins import ScopedFilterMixin
 from dcim.models import Device
@@ -397,6 +397,7 @@ class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
     vid_ranges: Annotated['IntegerRangeArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
         strawberry_django.filter_field()
     )
+    total_vlan_ids: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
 
 
 @strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)

+ 1 - 0
netbox/ipam/graphql/types.py

@@ -305,6 +305,7 @@ class VLANGroupType(OrganizationalObjectType):
 
     vlans: list[VLANType]
     vid_ranges: list[str]
+    total_vlan_ids: BigInt
     tenant: Annotated['TenantType', strawberry.lazy('tenancy.graphql.types')] | None
 
     @strawberry_django.field

+ 15 - 0
netbox/ipam/migrations/0087_rename_vlangroup_total_vlan_ids.py

@@ -0,0 +1,15 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('ipam', '0086_gfk_indexes'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='vlangroup',
+            old_name='_total_vlan_ids',
+            new_name='total_vlan_ids',
+        ),
+    ]

+ 5 - 5
netbox/ipam/models/vlans.py

@@ -62,6 +62,9 @@ class VLANGroup(OrganizationalModel):
         verbose_name=_('VLAN ID ranges'),
         default=default_vid_ranges
     )
+    total_vlan_ids = models.PositiveBigIntegerField(
+        default=VLAN_VID_MAX - VLAN_VID_MIN + 1,
+    )
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         on_delete=models.PROTECT,
@@ -69,9 +72,6 @@ class VLANGroup(OrganizationalModel):
         blank=True,
         null=True
     )
-    _total_vlan_ids = models.PositiveBigIntegerField(
-        default=VLAN_VID_MAX - VLAN_VID_MIN + 1
-    )
 
     objects = VLANGroupQuerySet.as_manager()
 
@@ -130,10 +130,10 @@ class VLANGroup(OrganizationalModel):
             raise ValidationError({'vid_ranges': _("Ranges cannot overlap.")})
 
     def save(self, *args, **kwargs):
-        self._total_vlan_ids = 0
+        self.total_vlan_ids = 0
         for vid_range in self.vid_ranges:
             # VID range is inclusive on lower-bound, exclusive on upper-bound
-            self._total_vlan_ids += vid_range.upper - vid_range.lower
+            self.total_vlan_ids += vid_range.upper - vid_range.lower
 
         super().save(*args, **kwargs)
 

+ 1 - 1
netbox/ipam/querysets.py

@@ -64,7 +64,7 @@ class VLANGroupQuerySet(RestrictedQuerySet):
 
         return self.annotate(
             vlan_count=count_related(VLAN, 'group'),
-            utilization=Round(F('vlan_count') * 100.0 / F('_total_vlan_ids'), 2)
+            utilization=Round(F('vlan_count') * 100.0 / F('total_vlan_ids'), 2),
         )
 
 

+ 6 - 2
netbox/ipam/tables/vlans.py

@@ -53,6 +53,9 @@ class VLANGroupTable(TenancyColumnsMixin, OrganizationalModelTable):
         url_params={'group_id': 'pk'},
         verbose_name=_('VLANs')
     )
+    total_vlan_ids = tables.Column(
+        verbose_name=_('Total VLAN IDs'),
+    )
     utilization = columns.UtilizationColumn(
         orderable=False,
         verbose_name=_('Utilization')
@@ -67,8 +70,9 @@ class VLANGroupTable(TenancyColumnsMixin, OrganizationalModelTable):
     class Meta(OrganizationalModelTable.Meta):
         model = VLANGroup
         fields = (
-            'pk', 'id', 'name', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count', 'slug', 'description',
-            'tenant', 'tenant_group', 'comments', 'tags', 'created', 'last_updated', 'actions', 'utilization',
+            'pk', 'id', 'name', 'slug', 'description', 'scope_type', 'scope', 'vid_ranges_list', 'vlan_count',
+            'total_vlan_ids', 'tenant', 'tenant_group', 'comments', 'tags', 'created', 'last_updated', 'actions',
+            'utilization',
         )
         default_columns = (
             'pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'tenant', 'description'

+ 1 - 1
netbox/ipam/tests/test_models.py

@@ -663,7 +663,7 @@ class TestVLANGroup(TestCase):
 
     def test_total_vlan_ids(self):
         vlangroup = VLANGroup.objects.first()
-        self.assertEqual(vlangroup._total_vlan_ids, 100)
+        self.assertEqual(vlangroup.total_vlan_ids, 100)
 
 
 class TestVLAN(TestCase):