Fixes #22588: Filter VLANs by Site Group scope when assigning to a prefix (#22684)

This commit is contained in:
Sri Chandraja Reddy Allala 2026-07-22 15:44:09 -04:00 committed by GitHub
parent 84a024babd
commit 3e3d36cc2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 52 additions and 6 deletions

View File

@ -1069,6 +1069,10 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
queryset=Site.objects.all(),
method='get_for_site'
)
available_at_site_group = django_filters.ModelChoiceFilter(
queryset=SiteGroup.objects.all(),
method='get_for_site_group'
)
available_on_device = django_filters.ModelChoiceFilter(
queryset=Device.objects.select_related('cluster'),
method='get_for_device'
@ -1130,6 +1134,10 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
def get_for_site(self, queryset, name, value):
return queryset.get_for_site(value)
@extend_schema_field(OpenApiTypes.STR)
def get_for_site_group(self, queryset, name, value):
return queryset.get_for_site_group(value)
@extend_schema_field(OpenApiTypes.STR)
def get_for_device(self, queryset, name, value):
return queryset.get_for_device(value)

View File

@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from dcim.forms.mixins import ScopedForm
from dcim.models import Device, Interface, Site
from dcim.models import Device, Interface, Site, SiteGroup
from ipam.choices import *
from ipam.constants import *
from ipam.formfields import IPNetworkFormField
@ -245,9 +245,17 @@ class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# #18605: only filter VLAN select list if scope field is a Site
# #18605: only filter VLAN select list if scope field is a Site or Site Group
if scope_field := self.fields.get('scope', None):
if scope_field.queryset.model is not Site:
if scope_field.queryset.model is Site:
pass # already filtered by available_at_site
elif scope_field.queryset.model is SiteGroup:
self.fields['vlan'].widget.dynamic_params.clear()
self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None)
self.fields['vlan'].widget.add_query_params({
'available_at_site_group': '$scope',
})
else:
self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None)

View File

@ -290,6 +290,23 @@ class VLANQuerySet(RestrictedQuerySet):
Q(group__isnull=True, site__isnull=True) # Global VLANs
)
def get_for_site_group(self, site_group):
"""
Return all VLANs available to the specified site group.
"""
if site_group is None:
return self.none()
from .models import VLANGroup
q = Q(
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
scope_id__in=site_group.get_ancestors(include_self=True)
)
return self.filter(
Q(group__in=VLANGroup.objects.filter(q)) |
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs
)
def get_for_device(self, device):
"""
Return all VLANs available to the specified Device.

View File

@ -2221,6 +2221,11 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'available_at_site': site_id}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) # 4 scoped + 1 global group + 1 global
def test_available_at_site_group(self):
site_group = SiteGroup.objects.get(name='Site Group 1')
params = {'available_at_site_group': site_group.pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) # 1 scoped + 1 global group + 1 global
def test_interface(self):
interface_id = Interface.objects.first().pk
params = {'interface_id': interface_id}

View File

@ -29,19 +29,27 @@ class PrefixFormTestCase(TestCase):
assert form.fields['vlan'].widget.attrs['data-dynamic-params'] == self.default_dynamic_params
def test_vlan_field_sets_dynamic_params_for_scope_site_group(self):
"""data-dynamic-params present with available_at_site_group when scope type is Site Group"""
site_group = SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
form = PrefixForm(data={
'scope_type': ContentType.objects.get_for_model(SiteGroup).id,
'scope': site_group,
})
expected = '[{"fieldName":"scope","queryParam":"available_at_site_group"}]'
assert form.fields['vlan'].widget.attrs['data-dynamic-params'] == expected
def test_vlan_field_does_not_set_dynamic_params_for_other_scopes(self):
"""data-dynamic-params not present when scope type is populated by is not Site"""
"""data-dynamic-params not present when scope type is not Site or Site Group"""
cases = [
Region(name='Region 1', slug='region-1'),
Location(site=self.site, name='Location 1', slug='location-1'),
SiteGroup(name='Site Group 1', slug='site-group-1'),
]
for case in cases:
form = PrefixForm(data={
'scope_type': ContentType.objects.get_for_model(case._meta.model).id,
'scope': case,
})
assert 'data-dynamic-params' not in form.fields['vlan'].widget.attrs