diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 9187901f0..03d14a160 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -757,7 +757,7 @@ class CableSerializer(PrimaryModelSerializer): ) termination_a = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True) - status = ChoiceField(choices=CableStatusChoices, required=False) + status = ChoiceField(choices=LinkStatusChoices, required=False) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False) class Meta: diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 9a78a74f9..d58d5466d 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1030,7 +1030,7 @@ class PortTypeChoices(ChoiceSet): # -# Cables +# Cables/links # class CableTypeChoices(ChoiceSet): @@ -1094,7 +1094,7 @@ class CableTypeChoices(ChoiceSet): ) -class CableStatusChoices(ChoiceSet): +class LinkStatusChoices(ChoiceSet): STATUS_CONNECTED = 'connected' STATUS_PLANNED = 'planned' diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 0c756957a..f778fd083 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1205,7 +1205,7 @@ class CableFilterSet(PrimaryModelFilterSet): choices=CableTypeChoices ) status = django_filters.MultipleChoiceFilter( - choices=CableStatusChoices + choices=LinkStatusChoices ) color = django_filters.MultipleChoiceFilter( choices=ColorChoices diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 67a482a26..f710e7d8c 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -453,7 +453,7 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE widget=StaticSelect() ) status = forms.ChoiceField( - choices=add_blank_choice(CableStatusChoices), + choices=add_blank_choice(LinkStatusChoices), required=False, widget=StaticSelect(), initial='' diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index a2685c8e0..95b4dd136 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -807,7 +807,7 @@ class CableCSVForm(CustomFieldModelCSVForm): # Cable attributes status = CSVChoiceField( - choices=CableStatusChoices, + choices=LinkStatusChoices, required=False, help_text='Connection status' ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 605139c1b..169b93028 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -732,7 +732,7 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm): ) status = forms.ChoiceField( required=False, - choices=add_blank_choice(CableStatusChoices), + choices=add_blank_choice(LinkStatusChoices), widget=StaticSelect() ) color = ColorField( diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index fb3e71543..129617746 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -64,8 +64,8 @@ class Cable(PrimaryModel): ) status = models.CharField( max_length=50, - choices=CableStatusChoices, - default=CableStatusChoices.STATUS_CONNECTED + choices=LinkStatusChoices, + default=LinkStatusChoices.STATUS_CONNECTED ) label = models.CharField( max_length=100, @@ -285,7 +285,7 @@ class Cable(PrimaryModel): self._pk = self.pk def get_status_class(self): - return CableStatusChoices.CSS_CLASSES.get(self.status) + return LinkStatusChoices.CSS_CLASSES.get(self.status) def get_compatible_types(self): """ @@ -390,7 +390,7 @@ class CablePath(BigIDModel): node = origin while node.link is not None: - if hasattr(node.link, 'status') and node.link.status != CableStatusChoices.STATUS_CONNECTED: + if hasattr(node.link, 'status') and node.link.status != LinkStatusChoices.STATUS_CONNECTED: is_active = False # Follow the link to its far-end termination diff --git a/netbox/dcim/signals.py b/netbox/dcim/signals.py index 616546525..79e9c6687 100644 --- a/netbox/dcim/signals.py +++ b/netbox/dcim/signals.py @@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models.signals import post_save, post_delete, pre_delete from django.dispatch import receiver -from .choices import CableStatusChoices +from .choices import LinkStatusChoices from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis from .utils import create_cablepath, rebuild_paths @@ -102,7 +102,7 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs): # We currently don't support modifying either termination of an existing Cable. (This # may change in the future.) However, we do need to capture status changes and update # any CablePaths accordingly. - if instance.status != CableStatusChoices.STATUS_CONNECTED: + if instance.status != LinkStatusChoices.STATUS_CONNECTED: CablePath.objects.filter(path__contains=instance).update(is_active=False) else: rebuild_paths(instance) diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index c0fc89f83..6849df012 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from circuits.models import * -from dcim.choices import CableStatusChoices +from dcim.choices import LinkStatusChoices from dcim.models import * from dcim.utils import object_to_path_node @@ -1142,7 +1142,7 @@ class CablePathTestCase(TestCase): self.assertEqual(CablePath.objects.count(), 2) # Change cable 2's status to "planned" - cable2.status = CableStatusChoices.STATUS_PLANNED + cable2.status = LinkStatusChoices.STATUS_PLANNED cable2.save() self.assertPathExists( origin=interface1, @@ -1160,7 +1160,7 @@ class CablePathTestCase(TestCase): # Change cable 2's status to "connected" cable2 = Cable.objects.get(pk=cable2.pk) - cable2.status = CableStatusChoices.STATUS_CONNECTED + cable2.status = LinkStatusChoices.STATUS_CONNECTED cable2.save() self.assertPathExists( origin=interface1, diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index fb94bde08..0c1ffd54b 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -2855,12 +2855,12 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1') # Cables - Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=CableStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=CableStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() - Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() - Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=CableStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save() + Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save() + Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save() Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save() def test_label(self): @@ -2880,9 +2880,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_status(self): - params = {'status': [CableStatusChoices.STATUS_CONNECTED]} + params = {'status': [LinkStatusChoices.STATUS_CONNECTED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) - params = {'status': [CableStatusChoices.STATUS_PLANNED]} + params = {'status': [LinkStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_color(self): diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 545a56f81..c0af2d438 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1813,7 +1813,7 @@ class CableTestCase( 'termination_b_type': interface_ct.pk, 'termination_b_id': interfaces[3].pk, 'type': CableTypeChoices.TYPE_CAT6, - 'status': CableStatusChoices.STATUS_PLANNED, + 'status': LinkStatusChoices.STATUS_PLANNED, 'label': 'Label', 'color': 'c0c0c0', 'length': 100, @@ -1830,7 +1830,7 @@ class CableTestCase( cls.bulk_edit_data = { 'type': CableTypeChoices.TYPE_CAT5E, - 'status': CableStatusChoices.STATUS_CONNECTED, + 'status': LinkStatusChoices.STATUS_CONNECTED, 'label': 'New label', 'color': '00ff00', 'length': 50, diff --git a/netbox/templates/wireless/wirelesslink.html b/netbox/templates/wireless/wirelesslink.html index 6196adae4..45ec6b0c9 100644 --- a/netbox/templates/wireless/wirelesslink.html +++ b/netbox/templates/wireless/wirelesslink.html @@ -15,6 +15,10 @@
Status | +{{ object.get_status_display }} | +
---|---|
SSID | {{ object.ssid|placeholder }} | diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 9337d6864..5a7330129 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,7 +1,9 @@ from rest_framework import serializers +from dcim.choices import LinkStatusChoices from dcim.api.serializers import NestedInterfaceSerializer from ipam.api.serializers import NestedVLANSerializer +from netbox.api import ChoiceField from netbox.api.serializers import PrimaryModelSerializer from wireless.models import * from .nested_serializers import * @@ -25,11 +27,12 @@ class WirelessLANSerializer(PrimaryModelSerializer): class WirelessLinkSerializer(PrimaryModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') + status = ChoiceField(choices=LinkStatusChoices, required=False) interface_a = NestedInterfaceSerializer() interface_b = NestedInterfaceSerializer() class Meta: model = WirelessLink fields = [ - 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'description', + 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', ] diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 7341ada9d..f765172dd 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -1,6 +1,7 @@ import django_filters from django.db.models import Q +from dcim.choices import LinkStatusChoices from extras.filters import TagFilter from netbox.filtersets import PrimaryModelFilterSet from .models import * @@ -37,6 +38,9 @@ class WirelessLinkFilterSet(PrimaryModelFilterSet): method='search', label='Search', ) + status = django_filters.MultipleChoiceFilter( + choices=LinkStatusChoices + ) tag = TagFilter() class Meta: diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 65666ccb1..72af81f56 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -1,10 +1,11 @@ from django import forms -from dcim.models import * +from dcim.choices import LinkStatusChoices from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import VLAN from utilities.forms import BootstrapMixin, DynamicModelChoiceField from wireless.constants import SSID_MAX_LENGTH +from wireless.models import * __all__ = ( 'WirelessLANBulkEditForm', @@ -14,7 +15,7 @@ __all__ = ( class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( - queryset=PowerFeed.objects.all(), + queryset=WirelessLAN.objects.all(), widget=forms.MultipleHiddenInput ) vlan = DynamicModelChoiceField( @@ -35,13 +36,17 @@ class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm): pk = forms.ModelMultipleChoiceField( - queryset=PowerFeed.objects.all(), + queryset=WirelessLink.objects.all(), widget=forms.MultipleHiddenInput ) ssid = forms.CharField( max_length=SSID_MAX_LENGTH, required=False ) + status = forms.ChoiceField( + choices=LinkStatusChoices, + required=False + ) description = forms.CharField( required=False ) diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index caf322dc1..763305c38 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -1,7 +1,8 @@ +from dcim.choices import LinkStatusChoices from dcim.models import Interface from extras.forms import CustomFieldModelCSVForm from ipam.models import VLAN -from utilities.forms import CSVModelChoiceField +from utilities.forms import CSVChoiceField, CSVModelChoiceField from wireless.models import * __all__ = ( @@ -23,6 +24,10 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm): class WirelessLinkCSVForm(CustomFieldModelCSVForm): + status = CSVChoiceField( + choices=LinkStatusChoices, + help_text='Connection status' + ) interface_a = CSVModelChoiceField( queryset=Interface.objects.all() ) diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index fa1912099..6da3cd716 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -1,8 +1,9 @@ from django import forms from django.utils.translation import gettext as _ +from dcim.choices import LinkStatusChoices from extras.forms import CustomFieldModelFilterForm -from utilities.forms import BootstrapMixin, TagFilterField +from utilities.forms import add_blank_choice, BootstrapMixin, StaticSelect, TagFilterField from wireless.models import * __all__ = ( @@ -42,4 +43,9 @@ class WirelessLinkFilterForm(BootstrapMixin, CustomFieldModelFilterForm): required=False, label='SSID' ) + status = forms.ChoiceField( + required=False, + choices=add_blank_choice(LinkStatusChoices), + widget=StaticSelect() + ) tag = TagFilterField(model) diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index c494fb5a2..174eb5983 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -2,7 +2,7 @@ from dcim.models import Interface from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN -from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect from wireless.models import * __all__ = ( @@ -55,5 +55,8 @@ class WirelessLinkForm(BootstrapMixin, CustomFieldModelForm): class Meta: model = WirelessLink fields = [ - 'interface_a', 'interface_b', 'ssid', 'description', 'tags', + 'interface_a', 'interface_b', 'status', 'ssid', 'description', 'tags', ] + widgets = { + 'status': StaticSelect, + } diff --git a/netbox/wireless/migrations/0001_wireless.py b/netbox/wireless/migrations/0001_wireless.py index 2fb07e5fd..8eb042d7d 100644 --- a/netbox/wireless/migrations/0001_wireless.py +++ b/netbox/wireless/migrations/0001_wireless.py @@ -42,6 +42,7 @@ class Migration(migrations.Migration): ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)), ('id', models.BigAutoField(primary_key=True, serialize=False)), ('ssid', models.CharField(blank=True, max_length=32)), + ('status', models.CharField(default='connected', max_length=50)), ('description', models.CharField(blank=True, max_length=200)), ('_interface_a_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')), ('_interface_b_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='dcim.device')), diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index f8c947385..d02358f1c 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse +from dcim.choices import LinkStatusChoices from dcim.constants import WIRELESS_IFACE_TYPES from extras.utils import extras_features from netbox.models import BigIDModel, PrimaryModel @@ -70,6 +71,11 @@ class WirelessLink(PrimaryModel): blank=True, verbose_name='SSID' ) + status = models.CharField( + max_length=50, + choices=LinkStatusChoices, + default=LinkStatusChoices.STATUS_CONNECTED + ) description = models.CharField( max_length=200, blank=True @@ -94,6 +100,8 @@ class WirelessLink(PrimaryModel): objects = RestrictedQuerySet.as_manager() + clone_fields = ('ssid', 'status') + class Meta: ordering = ['pk'] unique_together = ('interface_a', 'interface_b') @@ -104,6 +112,9 @@ class WirelessLink(PrimaryModel): def get_absolute_url(self): return reverse('wireless:wirelesslink', args=[self.pk]) + def get_status_class(self): + return LinkStatusChoices.CSS_CLASSES.get(self.status) + def clean(self): # Validate interface types diff --git a/netbox/wireless/tables.py b/netbox/wireless/tables.py index 31c9e56a8..9b0ef7291 100644 --- a/netbox/wireless/tables.py +++ b/netbox/wireless/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from .models import * -from utilities.tables import BaseTable, TagColumn, ToggleColumn +from utilities.tables import BaseTable, ChoiceFieldColumn, TagColumn, ToggleColumn __all__ = ( 'WirelessLANTable', @@ -30,6 +30,7 @@ class WirelessLinkTable(BaseTable): linkify=True, verbose_name='ID' ) + status = ChoiceFieldColumn() interface_a = tables.Column( linkify=True ) @@ -42,5 +43,5 @@ class WirelessLinkTable(BaseTable): class Meta(BaseTable.Meta): model = WirelessLink - fields = ('pk', 'id', 'interface_a', 'interface_b', 'ssid', 'description') - default_columns = ('pk', 'id', 'interface_a', 'interface_b', 'ssid', 'description') + fields = ('pk', 'id', 'status', 'interface_a', 'interface_b', 'ssid', 'description') + default_columns = ('pk', 'id', 'status', 'interface_a', 'interface_b', 'ssid', 'description')