From e6c1cebd340a5b4fdcf4827c93a437ced1f310b2 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Mon, 16 Jun 2025 20:19:56 +0200 Subject: [PATCH] Closes #19499 - Add WirelessLink Bulk Import Support by Device and Interface Names (#19679) --- netbox/wireless/forms/bulk_import.py | 75 ++++++++++++++++++++++++---- netbox/wireless/models.py | 4 +- netbox/wireless/tests/test_views.py | 8 +-- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 389dcf25d..29395f814 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from dcim.choices import LinkStatusChoices from dcim.forms.mixins import ScopedImportForm -from dcim.models import Interface +from dcim.models import Device, Interface, Site from ipam.models import VLAN from netbox.choices import * from netbox.forms import NetBoxModelImportForm @@ -85,18 +85,53 @@ class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm): class WirelessLinkImportForm(NetBoxModelImportForm): - status = CSVChoiceField( - label=_('Status'), - choices=LinkStatusChoices, - help_text=_('Connection status') + # Termination A + site_a = CSVModelChoiceField( + label=_('Site A'), + queryset=Site.objects.all(), + required=False, + to_field_name='name', + help_text=_('Site of parent device A (if any)'), + ) + device_a = CSVModelChoiceField( + label=_('Device A'), + queryset=Device.objects.all(), + to_field_name='name', + help_text=_('Parent device of assigned interface A'), ) interface_a = CSVModelChoiceField( label=_('Interface A'), - queryset=Interface.objects.all() + queryset=Interface.objects.all(), + to_field_name='name', + help_text=_('Assigned interface A'), + ) + + # Termination B + site_b = CSVModelChoiceField( + label=_('Site B'), + queryset=Site.objects.all(), + required=False, + to_field_name='name', + help_text=_('Site of parent device B (if any)'), + ) + device_b = CSVModelChoiceField( + label=_('Device B'), + queryset=Device.objects.all(), + to_field_name='name', + help_text=_('Parent device of assigned interface B'), ) interface_b = CSVModelChoiceField( label=_('Interface B'), - queryset=Interface.objects.all() + queryset=Interface.objects.all(), + to_field_name='name', + help_text=_('Assigned interface B'), + ) + + # WirelessLink attributes + status = CSVChoiceField( + label=_('Status'), + choices=LinkStatusChoices, + help_text=_('Connection status'), ) tenant = CSVModelChoiceField( label=_('Tenant'), @@ -127,6 +162,28 @@ class WirelessLinkImportForm(NetBoxModelImportForm): class Meta: model = WirelessLink fields = ( - 'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', - 'distance', 'distance_unit', 'description', 'comments', 'tags', + 'site_a', 'device_a', 'interface_a', 'site_b', 'device_b', 'interface_b', 'status', 'ssid', 'tenant', + 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', 'comments', 'tags', ) + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + # Limit choices for interface_a to the assigned device_a + interface_a_params = {f'device__{self.fields["device_a"].to_field_name}': data.get('device_a')} + # Limit choices for device_a to the assigned site_a + if site_a := data.get('site_a'): + device_a_params = {f'site__{self.fields["site_a"].to_field_name}': site_a} + self.fields['device_a'].queryset = self.fields['device_a'].queryset.filter(**device_a_params) + interface_a_params.update({f'device__site__{self.fields["site_a"].to_field_name}': site_a}) + self.fields['interface_a'].queryset = self.fields['interface_a'].queryset.filter(**interface_a_params) + + # Limit choices for interface_b to the assigned device_b + interface_b_params = {f'device__{self.fields["device_b"].to_field_name}': data.get('device_b')} + # Limit choices for device_b to the assigned site_b + if site_b := data.get('site_b'): + device_b_params = {f'site__{self.fields["site_b"].to_field_name}': site_b} + self.fields['device_b'].queryset = self.fields['device_b'].queryset.filter(**device_b_params) + interface_b_params.update({f'device__site__{self.fields["site_b"].to_field_name}': site_b}) + self.fields['interface_b'].queryset = self.fields['interface_b'].queryset.filter(**interface_b_params) diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index 9c73ae5b7..11f9e06eb 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -198,13 +198,13 @@ class WirelessLink(WirelessAuthenticationBase, DistanceMixin, PrimaryModel): super().clean() # Validate interface types - if self.interface_a.type not in WIRELESS_IFACE_TYPES: + if hasattr(self, "interface_a") and self.interface_a.type not in WIRELESS_IFACE_TYPES: raise ValidationError({ 'interface_a': _( "{type} is not a wireless interface." ).format(type=self.interface_a.get_type_display()) }) - if self.interface_b.type not in WIRELESS_IFACE_TYPES: + if hasattr(self, "interface_b") and self.interface_b.type not in WIRELESS_IFACE_TYPES: raise ValidationError({ 'interface_b': _( "{type} is not a wireless interface." diff --git a/netbox/wireless/tests/test_views.py b/netbox/wireless/tests/test_views.py index 975f18c0d..587ae7f89 100644 --- a/netbox/wireless/tests/test_views.py +++ b/netbox/wireless/tests/test_views.py @@ -198,10 +198,10 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "interface_a,interface_b,status,tenant", - f"{interfaces[6].pk},{interfaces[7].pk},connected,{tenants[0].name}", - f"{interfaces[8].pk},{interfaces[9].pk},connected,{tenants[1].name}", - f"{interfaces[10].pk},{interfaces[11].pk},connected,{tenants[2].name}", + "device_a,interface_a,device_b,interface_b,status,tenant", + f"{interfaces[6].device.name},{interfaces[6].name},{interfaces[7].device.name},{interfaces[7].name},connected,{tenants[0].name}", + f"{interfaces[8].device.name},{interfaces[8].name},{interfaces[9].device.name},{interfaces[9].name},connected,{tenants[1].name}", + f"{interfaces[10].device.name},{interfaces[10].name},{interfaces[11].device.name},{interfaces[11].name},connected,{tenants[2].name}", ) cls.csv_update_data = (