diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index ad72e0735..3af825d30 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.1.8
+ placeholder: v3.1.9
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 68256471c..f5bf198b8 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.1.8
+ placeholder: v3.1.9
validations:
required: true
- type: dropdown
diff --git a/README.md b/README.md
index 8429cd4b3..42bf8b619 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
+:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
+

NetBox is an infrastructure resource modeling (IRM) tool designed to empower
diff --git a/docs/index.md b/docs/index.md
index 943f1d7ab..5742c0ab6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,5 +1,7 @@
{style="height: 100px; margin-bottom: 3em"}
+:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
+
# What is NetBox?
NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management:
diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md
index f6b1292b8..3b02b3c42 100644
--- a/docs/release-notes/version-3.1.md
+++ b/docs/release-notes/version-3.1.md
@@ -1,13 +1,35 @@
# NetBox v3.1
-## v3.1.9 (FUTURE)
+## v3.1.10 (FUTURE)
+
+* [#8820](https://github.com/netbox-community/netbox/issues/8820) - Fix navbar background color in dark mode
+
+---
+
+## v3.1.9 (2022-03-07)
+
+### Enhancements
+
+* [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models
+* [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function
+* [#8664](https://github.com/netbox-community/netbox/issues/8664) - Show assigned ASNs/sites under list views
+* [#8736](https://github.com/netbox-community/netbox/issues/8736) - Add PC and UPC fiber end faces for LC/SC/LSH port types
+* [#8758](https://github.com/netbox-community/netbox/issues/8758) - Allow empty string substitution when renaming objects in bulk
+* [#8762](https://github.com/netbox-community/netbox/issues/8762) - Link to rack elevations list from site view
+* [#8766](https://github.com/netbox-community/netbox/issues/8766) - Add SCTP to service protocols list
### Bug Fixes
* [#8546](https://github.com/netbox-community/netbox/issues/8546) - Fix bulk import to restrict bridge, parent, and LAG to device interfaces
+* [#8633](https://github.com/netbox-community/netbox/issues/8633) - Prevent navigation sidebar pin from disappearing at certain breakpoints
* [#8674](https://github.com/netbox-community/netbox/issues/8674) - Fix rendering of tabbed content in documentation
+* [#8710](https://github.com/netbox-community/netbox/issues/8710) - Fix dynamic scope selection form fields when creating a VLAN group
* [#8713](https://github.com/netbox-community/netbox/issues/8713) - Restore missing "add" button on services list view
+* [#8715](https://github.com/netbox-community/netbox/issues/8715) - Avoid returning multiple objects when restricting querysets using multiple tags in permissions
* [#8717](https://github.com/netbox-community/netbox/issues/8717) - Fix redirection after bulk edit/delete of prefixes from aggregate view
+* [#8724](https://github.com/netbox-community/netbox/issues/8724) - Fix exception during device import with invalid device type
+* [#8807](https://github.com/netbox-community/netbox/issues/8807) - Correct REST API URL for FHRP group assignments
+* [#8808](https://github.com/netbox-community/netbox/issues/8808) - Fix members count under FHRP group list
---
diff --git a/mkdocs.yml b/mkdocs.yml
index 4be5b16e7..6245f3bb2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,11 +8,13 @@ theme:
icon:
repo: fontawesome/brands/github
palette:
- - scheme: default
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
toggle:
icon: material/lightbulb-outline
name: Switch to Dark Mode
- - scheme: slate
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
toggle:
icon: material/lightbulb
name: Switch to Light Mode
diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py
index 5e3e00301..0680dd5a1 100644
--- a/netbox/circuits/filtersets.py
+++ b/netbox/circuits/filtersets.py
@@ -98,7 +98,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
class Meta:
model = ProviderNetwork
- fields = ['id', 'name']
+ fields = ['id', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -115,7 +115,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = CircuitType
- fields = ['id', 'name', 'slug']
+ fields = ['id', 'name', 'slug', 'description']
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@@ -193,7 +193,7 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
class Meta:
model = Circuit
- fields = ['id', 'cid', 'install_date', 'commit_rate']
+ fields = ['id', 'cid', 'description', 'install_date', 'commit_rate']
def search(self, queryset, name, value):
if not value.strip():
@@ -234,7 +234,7 @@ class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFi
class Meta:
model = CircuitTermination
- fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
+ fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description']
def search(self, queryset, name, value):
if not value.strip():
diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py
index 4880a8388..20416c4e6 100644
--- a/netbox/circuits/tests/test_filtersets.py
+++ b/netbox/circuits/tests/test_filtersets.py
@@ -108,8 +108,8 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
CircuitType.objects.bulk_create((
- CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
- CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
+ CircuitType(name='Circuit Type 1', slug='circuit-type-1', description='foobar1'),
+ CircuitType(name='Circuit Type 2', slug='circuit-type-2', description='foobar2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
))
@@ -121,6 +121,10 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['circuit-type-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Circuit.objects.all()
@@ -187,8 +191,8 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
- Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
- Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
+ Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
+ Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
Circuit(provider=providers[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
@@ -241,6 +245,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -319,8 +327,8 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
- CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC'),
- CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF'),
+ CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
+ CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
@@ -349,6 +357,10 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'xconnect_id': ['ABC', 'DEF']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_circuit_id(self):
circuits = Circuit.objects.all()[:2]
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
@@ -386,8 +398,8 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider.objects.bulk_create(providers)
provider_networks = (
- ProviderNetwork(name='Provider Network 1', provider=providers[0]),
- ProviderNetwork(name='Provider Network 2', provider=providers[1]),
+ ProviderNetwork(name='Provider Network 1', provider=providers[0], description='foobar1'),
+ ProviderNetwork(name='Provider Network 2', provider=providers[1], description='foobar2'),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
@@ -396,6 +408,10 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'name': ['Provider Network 1', 'Provider Network 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index 1d3b59497..8900b2d89 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -1013,13 +1013,19 @@ class PortTypeChoices(ChoiceSet):
TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st'
TYPE_SC = 'sc'
+ TYPE_SC_PC = 'sc-pc'
+ TYPE_SC_UPC = 'sc-upc'
TYPE_SC_APC = 'sc-apc'
TYPE_FC = 'fc'
TYPE_LC = 'lc'
+ TYPE_LC_PC = 'lc-pc'
+ TYPE_LC_UPC = 'lc-upc'
TYPE_LC_APC = 'lc-apc'
TYPE_MTRJ = 'mtrj'
TYPE_MPO = 'mpo'
TYPE_LSH = 'lsh'
+ TYPE_LSH_PC = 'lsh-pc'
+ TYPE_LSH_UPC = 'lsh-upc'
TYPE_LSH_APC = 'lsh-apc'
TYPE_SPLICE = 'splice'
TYPE_CS = 'cs'
@@ -1059,12 +1065,18 @@ class PortTypeChoices(ChoiceSet):
(
(TYPE_FC, 'FC'),
(TYPE_LC, 'LC'),
+ (TYPE_LC_PC, 'LC/PC'),
+ (TYPE_LC_UPC, 'LC/UPC'),
(TYPE_LC_APC, 'LC/APC'),
(TYPE_LSH, 'LSH'),
+ (TYPE_LSH_PC, 'LSH/PC'),
+ (TYPE_LSH_UPC, 'LSH/UPC'),
(TYPE_LSH_APC, 'LSH/APC'),
(TYPE_MPO, 'MPO'),
(TYPE_MTRJ, 'MTRJ'),
(TYPE_SC, 'SC'),
+ (TYPE_SC_PC, 'SC/PC'),
+ (TYPE_SC_UPC, 'SC/UPC'),
(TYPE_SC_APC, 'SC/APC'),
(TYPE_ST, 'ST'),
(TYPE_CS, 'CS'),
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index c198335f4..504ad69ca 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -142,7 +142,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterS
model = Site
fields = [
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
- 'contact_email',
+ 'contact_email', 'description'
]
def search(self, queryset, name, value):
@@ -237,7 +237,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = RackRole
- fields = ['id', 'name', 'slug', 'color']
+ fields = ['id', 'name', 'slug', 'color', 'description']
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@@ -385,7 +385,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = RackReservation
- fields = ['id', 'created']
+ fields = ['id', 'created', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -586,7 +586,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = DeviceRole
- fields = ['id', 'name', 'slug', 'color', 'vm_role']
+ fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description']
class PlatformFilterSet(OrganizationalModelFilterSet):
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index a2ae20319..6b8ff043d 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -670,10 +670,11 @@ class Device(PrimaryModel, ConfigContextModel):
})
# Prevent 0U devices from being assigned to a specific position
- if self.position and self.device_type.u_height == 0:
- raise ValidationError({
- 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
- })
+ if hasattr(self, 'device_type'):
+ if self.position and self.device_type.u_height == 0:
+ raise ValidationError({
+ 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
+ })
if self.rack:
diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py
index 06753ff0f..5d41a37af 100644
--- a/netbox/dcim/tables/sites.py
+++ b/netbox/dcim/tables/sites.py
@@ -91,6 +91,10 @@ class SiteTable(BaseTable):
accessor=tables.A('asns__count'),
viewname='ipam:asn_list',
url_params={'site_id': 'pk'},
+ verbose_name='ASN Count'
+ )
+ asns = tables.ManyToManyColumn(
+ linkify_item=True,
verbose_name='ASNs'
)
tenant = TenantColumn()
@@ -105,9 +109,15 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = (
+<<<<<<< HEAD
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_phone', 'contact_email', 'contacts', 'comments', 'tags', 'created', 'last_updated',
+=======
+ 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
+ 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
+ 'contact_phone', 'contact_email', 'comments', 'tags', 'created', 'last_updated',
+>>>>>>> develop
)
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index ab290f791..68a4b5571 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -151,8 +151,8 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
ASN.objects.bulk_create(asns)
sites = (
- Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com'),
- Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com'),
+ Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10, contact_name='Contact 1', contact_phone='123-555-0001', contact_email='contact1@example.com', description='foobar1'),
+ Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20, contact_name='Contact 2', contact_phone='123-555-0002', contact_email='contact2@example.com', description='foobar2'),
Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30, contact_name='Contact 3', contact_phone='123-555-0003', contact_email='contact3@example.com'),
)
Site.objects.bulk_create(sites)
@@ -201,6 +201,10 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'contact_email': ['contact1@example.com', 'contact2@example.com']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_status(self):
params = {'status': [SiteStatusChoices.STATUS_ACTIVE, SiteStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -329,8 +333,8 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
rack_roles = (
- RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000'),
- RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00'),
+ RackRole(name='Rack Role 1', slug='rack-role-1', color='ff0000', description='foobar1'),
+ RackRole(name='Rack Role 2', slug='rack-role-2', color='00ff00', description='foobar2'),
RackRole(name='Rack Role 3', slug='rack-role-3', color='0000ff'),
)
RackRole.objects.bulk_create(rack_roles)
@@ -347,6 +351,10 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'color': ['ff0000', '00ff00']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Rack.objects.all()
@@ -570,8 +578,8 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
reservations = (
- RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0]),
- RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1]),
+ RackReservation(rack=racks[0], units=[1, 2, 3], user=users[0], tenant=tenants[0], description='foobar1'),
+ RackReservation(rack=racks[1], units=[4, 5, 6], user=users[1], tenant=tenants[1], description='foobar2'),
RackReservation(rack=racks[2], units=[7, 8, 9], user=users[2], tenant=tenants[2]),
)
RackReservation.objects.bulk_create(reservations)
@@ -604,6 +612,10 @@ class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
@@ -1088,8 +1100,8 @@ class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
device_roles = (
- DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True),
- DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True),
+ DeviceRole(name='Device Role 1', slug='device-role-1', color='ff0000', vm_role=True, description='foobar1'),
+ DeviceRole(name='Device Role 2', slug='device-role-2', color='00ff00', vm_role=True, description='foobar2'),
DeviceRole(name='Device Role 3', slug='device-role-3', color='0000ff', vm_role=False),
)
DeviceRole.objects.bulk_create(device_roles)
@@ -1112,6 +1124,10 @@ class DeviceRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'vm_role': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Platform.objects.all()
diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py
index 6867e598e..9dff088ae 100644
--- a/netbox/extras/filtersets.py
+++ b/netbox/extras/filtersets.py
@@ -62,7 +62,7 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta:
model = CustomField
- fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight']
+ fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -103,7 +103,7 @@ class ExportTemplateFilterSet(BaseFilterSet):
class Meta:
model = ExportTemplate
- fields = ['id', 'content_type', 'name']
+ fields = ['id', 'content_type', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -177,14 +177,15 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
class Meta:
model = Tag
- fields = ['id', 'name', 'slug', 'color']
+ fields = ['id', 'name', 'slug', 'color', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
- Q(slug__icontains=value)
+ Q(slug__icontains=value) |
+ Q(description__icontains=value)
)
def _content_type(self, queryset, name, values):
diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py
index 975fe7d7e..c94fb4998 100644
--- a/netbox/extras/tests/test_filtersets.py
+++ b/netbox/extras/tests/test_filtersets.py
@@ -153,8 +153,8 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
export_templates = (
- ExportTemplate(name='Export Template 1', content_type=content_types[0], template_code='TESTING'),
- ExportTemplate(name='Export Template 2', content_type=content_types[1], template_code='TESTING'),
+ ExportTemplate(name='Export Template 1', content_type=content_types[0], template_code='TESTING', description='foobar1'),
+ ExportTemplate(name='Export Template 2', content_type=content_types[1], template_code='TESTING', description='foobar2'),
ExportTemplate(name='Export Template 3', content_type=content_types[2], template_code='TESTING'),
)
ExportTemplate.objects.bulk_create(export_templates)
@@ -167,6 +167,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
params = {'content_type': ContentType.objects.get(model='site').pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
queryset = ImageAttachment.objects.all()
@@ -542,8 +546,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
tags = (
- Tag(name='Tag 1', slug='tag-1', color='ff0000'),
- Tag(name='Tag 2', slug='tag-2', color='00ff00'),
+ Tag(name='Tag 1', slug='tag-1', color='ff0000', description='foobar1'),
+ Tag(name='Tag 2', slug='tag-2', color='00ff00', description='foobar2'),
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
)
Tag.objects.bulk_create(tags)
@@ -567,6 +571,10 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'color': ['ff0000', '00ff00']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_content_type(self):
params = {'content_type': ['dcim.site', 'circuits.provider']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py
index aa1d2834a..a2c47aaec 100644
--- a/netbox/ipam/api/serializers.py
+++ b/netbox/ipam/api/serializers.py
@@ -126,7 +126,7 @@ class FHRPGroupSerializer(PrimaryModelSerializer):
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
+ url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer()
interface_type = ContentTypeField(
queryset=ContentType.objects.all()
diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py
index 526ef07d9..647a82810 100644
--- a/netbox/ipam/choices.py
+++ b/netbox/ipam/choices.py
@@ -189,8 +189,10 @@ class ServiceProtocolChoices(ChoiceSet):
PROTOCOL_TCP = 'tcp'
PROTOCOL_UDP = 'udp'
+ PROTOCOL_SCTP = 'sctp'
CHOICES = (
(PROTOCOL_TCP, 'TCP'),
(PROTOCOL_UDP, 'UDP'),
+ (PROTOCOL_SCTP, 'SCTP'),
)
diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py
index d7e1ee47e..5342d223d 100644
--- a/netbox/ipam/filtersets.py
+++ b/netbox/ipam/filtersets.py
@@ -75,7 +75,7 @@ class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = VRF
- fields = ['id', 'name', 'rd', 'enforce_unique']
+ fields = ['id', 'name', 'rd', 'enforce_unique', 'description']
class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
@@ -117,7 +117,7 @@ class RouteTargetFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = RouteTarget
- fields = ['id', 'name']
+ fields = ['id', 'name', 'description']
class RIRFilterSet(OrganizationalModelFilterSet):
@@ -155,7 +155,7 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = Aggregate
- fields = ['id', 'date_added']
+ fields = ['id', 'date_added', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -203,7 +203,7 @@ class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
class Meta:
model = ASN
- fields = ['id', 'asn']
+ fields = ['id', 'asn', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -225,7 +225,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = Role
- fields = ['id', 'name', 'slug']
+ fields = ['id', 'name', 'slug', 'description']
class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
@@ -354,7 +354,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = Prefix
- fields = ['id', 'is_pool', 'mark_utilized']
+ fields = ['id', 'is_pool', 'mark_utilized', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -460,7 +460,7 @@ class IPRangeFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
class Meta:
model = IPRange
- fields = ['id']
+ fields = ['id', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -839,7 +839,7 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = VLAN
- fields = ['id', 'vid', 'name']
+ fields = ['id', 'vid', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -891,7 +891,7 @@ class ServiceFilterSet(PrimaryModelFilterSet):
class Meta:
model = Service
- fields = ['id', 'name', 'protocol']
+ fields = ['id', 'name', 'protocol', 'description']
def search(self, queryset, name, value):
if not value.strip():
diff --git a/netbox/ipam/tables/fhrp.py b/netbox/ipam/tables/fhrp.py
index d615b6374..19f1c8c71 100644
--- a/netbox/ipam/tables/fhrp.py
+++ b/netbox/ipam/tables/fhrp.py
@@ -27,8 +27,8 @@ class FHRPGroupTable(BaseTable):
orderable=False,
verbose_name='IP Addresses'
)
- interface_count = tables.Column(
- verbose_name='Interfaces'
+ member_count = tables.Column(
+ verbose_name='Members'
)
tags = TagColumn(
url_name='ipam:fhrpgroup_list'
@@ -37,10 +37,10 @@ class FHRPGroupTable(BaseTable):
class Meta(BaseTable.Meta):
model = FHRPGroup
fields = (
- 'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'interface_count',
+ 'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count',
'tags', 'created', 'last_updated',
)
- default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'interface_count')
+ default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count')
class FHRPGroupAssignmentTable(BaseTable):
diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py
index 2cd2ab6ac..f1d490463 100644
--- a/netbox/ipam/tables/ip.py
+++ b/netbox/ipam/tables/ip.py
@@ -117,6 +117,10 @@ class ASNTable(BaseTable):
site_count = LinkedCountColumn(
viewname='dcim:site_list',
url_params={'asn_id': 'pk'},
+ verbose_name='Site Count'
+ )
+ sites = tables.ManyToManyColumn(
+ linkify_item=True,
verbose_name='Sites'
)
tenant = TenantColumn()
@@ -129,7 +133,7 @@ class ASNTable(BaseTable):
class Meta(BaseTable.Meta):
model = ASN
fields = (
- 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'actions', 'created',
+ 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'sites', 'actions', 'created',
'last_updated', 'tags',
)
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'description', 'tenant', 'actions')
diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py
index 773737dea..2f90e070a 100644
--- a/netbox/ipam/tests/test_filtersets.py
+++ b/netbox/ipam/tests/test_filtersets.py
@@ -35,8 +35,8 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
]
asns = (
- ASN(asn=64512, rir=rirs[0], tenant=tenants[0]),
- ASN(asn=64513, rir=rirs[0], tenant=tenants[0]),
+ ASN(asn=64512, rir=rirs[0], tenant=tenants[0], description='foobar1'),
+ ASN(asn=64513, rir=rirs[0], tenant=tenants[0], description='foobar2'),
ASN(asn=64514, rir=rirs[0], tenant=tenants[1]),
ASN(asn=64515, rir=rirs[0], tenant=tenants[2]),
ASN(asn=64516, rir=rirs[0], tenant=tenants[3]),
@@ -86,6 +86,10 @@ class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VRF.objects.all()
@@ -117,8 +121,8 @@ class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
vrfs = (
- VRF(name='VRF 1', rd='65000:100', tenant=tenants[0], enforce_unique=False),
- VRF(name='VRF 2', rd='65000:200', tenant=tenants[0], enforce_unique=False),
+ VRF(name='VRF 1', rd='65000:100', tenant=tenants[0], enforce_unique=False, description='foobar1'),
+ VRF(name='VRF 2', rd='65000:200', tenant=tenants[0], enforce_unique=False, description='foobar2'),
VRF(name='VRF 3', rd='65000:300', tenant=tenants[1], enforce_unique=False),
VRF(name='VRF 4', rd='65000:400', tenant=tenants[1], enforce_unique=True),
VRF(name='VRF 5', rd='65000:500', tenant=tenants[2], enforce_unique=True),
@@ -174,6 +178,10 @@ class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RouteTarget.objects.all()
@@ -198,8 +206,8 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
route_targets = (
- RouteTarget(name='65000:1001', tenant=tenants[0]),
- RouteTarget(name='65000:1002', tenant=tenants[0]),
+ RouteTarget(name='65000:1001', tenant=tenants[0], description='foobar1'),
+ RouteTarget(name='65000:1002', tenant=tenants[0], description='foobar2'),
RouteTarget(name='65000:1003', tenant=tenants[0]),
RouteTarget(name='65000:1004', tenant=tenants[0]),
RouteTarget(name='65000:2001', tenant=tenants[1]),
@@ -256,6 +264,10 @@ class RouteTargetTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class RIRTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RIR.objects.all()
@@ -323,8 +335,8 @@ class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
aggregates = (
- Aggregate(prefix='10.1.0.0/16', rir=rirs[0], tenant=tenants[0], date_added='2020-01-01'),
- Aggregate(prefix='10.2.0.0/16', rir=rirs[0], tenant=tenants[1], date_added='2020-01-02'),
+ Aggregate(prefix='10.1.0.0/16', rir=rirs[0], tenant=tenants[0], date_added='2020-01-01', description='foobar1'),
+ Aggregate(prefix='10.2.0.0/16', rir=rirs[0], tenant=tenants[1], date_added='2020-01-02', description='foobar2'),
Aggregate(prefix='10.3.0.0/16', rir=rirs[1], tenant=tenants[2], date_added='2020-01-03'),
Aggregate(prefix='2001:db8:1::/48', rir=rirs[1], tenant=tenants[0], date_added='2020-01-04'),
Aggregate(prefix='2001:db8:2::/48', rir=rirs[2], tenant=tenants[1], date_added='2020-01-05'),
@@ -340,6 +352,10 @@ class AggregateTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'date_added': ['2020-01-01', '2020-01-02']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
# TODO: Test for multiple values
def test_prefix(self):
params = {'prefix': '10.1.0.0/16'}
@@ -375,8 +391,8 @@ class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
roles = (
- Role(name='Role 1', slug='role-1'),
- Role(name='Role 2', slug='role-2'),
+ Role(name='Role 1', slug='role-1', description='foobar1'),
+ Role(name='Role 2', slug='role-2', description='foobar2'),
Role(name='Role 3', slug='role-3'),
)
Role.objects.bulk_create(roles)
@@ -389,6 +405,10 @@ class RoleTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['role-1', 'role-2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Prefix.objects.all()
@@ -467,8 +487,8 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
prefixes = (
- Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
- Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
+ Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True, description='foobar1'),
+ Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0], description='foobar2'),
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
@@ -601,6 +621,10 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPRange.objects.all()
@@ -639,8 +663,8 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants)
ip_ranges = (
- IPRange(start_address='10.0.1.100/24', end_address='10.0.1.199/24', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE),
- IPRange(start_address='10.0.2.100/24', end_address='10.0.2.199/24', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE),
+ IPRange(start_address='10.0.1.100/24', end_address='10.0.1.199/24', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar1'),
+ IPRange(start_address='10.0.2.100/24', end_address='10.0.2.199/24', size=100, vrf=vrfs[0], tenant=tenants[0], role=roles[0], status=IPRangeStatusChoices.STATUS_ACTIVE, description='foobar2'),
IPRange(start_address='10.0.3.100/24', end_address='10.0.3.199/24', size=100, vrf=vrfs[1], tenant=tenants[1], role=roles[1], status=IPRangeStatusChoices.STATUS_DEPRECATED),
IPRange(start_address='10.0.4.100/24', end_address='10.0.4.199/24', size=100, vrf=vrfs[2], tenant=tenants[2], role=roles[2], status=IPRangeStatusChoices.STATUS_RESERVED),
IPRange(start_address='2001:db8:0:1::1/64', end_address='2001:db8:0:1::100/64', size=100, vrf=None, tenant=None, role=None, status=IPRangeStatusChoices.STATUS_ACTIVE),
@@ -692,6 +716,10 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = IPAddress.objects.all()
@@ -1201,8 +1229,8 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
vlans = (
# Create one VLAN per VLANGroup
- VLAN(vid=1, name='Region 1', group=groups[0]),
- VLAN(vid=2, name='Region 2', group=groups[1]),
+ VLAN(vid=1, name='Region 1', group=groups[0], description='foobar1'),
+ VLAN(vid=2, name='Region 2', group=groups[1], description='foobar2'),
VLAN(vid=3, name='Region 3', group=groups[2]),
VLAN(vid=4, name='Site Group 1', group=groups[3]),
VLAN(vid=5, name='Site Group 2', group=groups[4]),
@@ -1271,6 +1299,10 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'group': [groups[0].slug, groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_role(self):
roles = Role.objects.all()[:2]
params = {'role_id': [roles[0].pk, roles[1].pk]}
@@ -1337,8 +1369,8 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
VirtualMachine.objects.bulk_create(virtual_machines)
services = (
- Service(device=devices[0], name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1001]),
- Service(device=devices[1], name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1002]),
+ Service(device=devices[0], name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1001], description='foobar1'),
+ Service(device=devices[1], name='Service 2', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[1002], description='foobar2'),
Service(device=devices[2], name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_UDP, ports=[1003]),
Service(virtual_machine=virtual_machines[0], name='Service 4', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2001]),
Service(virtual_machine=virtual_machines[1], name='Service 5', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[2002]),
@@ -1354,6 +1386,10 @@ class ServiceTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'protocol': ServiceProtocolChoices.PROTOCOL_TCP}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_port(self):
params = {'port': '1001'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
index f5501f54f..d16e00337 100644
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -19,7 +19,7 @@ from netbox.config import PARAMS
# Environment setup
#
-VERSION = '3.1.9-dev'
+VERSION = '3.1.10-dev'
# Hostname
HOSTNAME = platform.node()
diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css
index 752715e7c..b929f176a 100644
Binary files a/netbox/project-static/dist/netbox-dark.css and b/netbox/project-static/dist/netbox-dark.css differ
diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js
index f388b96cf..45abd19e6 100644
Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ
diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map
index 566476dde..c7e20fe9a 100644
Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ
diff --git a/netbox/project-static/src/forms/scopeSelector.ts b/netbox/project-static/src/forms/scopeSelector.ts
index e1c7014bf..14ef972f8 100644
--- a/netbox/project-static/src/forms/scopeSelector.ts
+++ b/netbox/project-static/src/forms/scopeSelector.ts
@@ -6,7 +6,16 @@ type ShowHideMap = {
*
* @example vlangroup_edit
*/
- [view: string]: {
+ [view: string]: string;
+};
+
+type ShowHideLayout = {
+ /**
+ * Name of layout config
+ *
+ * @example vlangroup
+ */
+ [config: string]: {
/**
* Default layout.
*/
@@ -19,15 +28,15 @@ type ShowHideMap = {
};
/**
- * Mapping of scope names to arrays of object types whose fields should be hidden or shown when
+ * Mapping of layout names to arrays of object types whose fields should be hidden or shown when
* the scope type (key) is selected.
*
* For example, if `region` is the scope type, the fields with IDs listed in
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
* showHideMap.region.show should be shown.
*/
-const showHideMap: ShowHideMap = {
- vlangroup_edit: {
+const showHideLayout: ShowHideLayout = {
+ vlangroup: {
region: {
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
show: ['id_region'],
@@ -70,6 +79,17 @@ const showHideMap: ShowHideMap = {
},
},
};
+
+/**
+ * Mapping of view names to layout configurations
+ *
+ * For example, if `vlangroup_add` is the view, use the layout configuration `vlangroup`.
+ */
+const showHideMap: ShowHideMap = {
+ vlangroup_add: 'vlangroup',
+ vlangroup_edit: 'vlangroup',
+};
+
/**
* Toggle visibility of a given element's parent.
* @param query CSS Query.
@@ -94,8 +114,9 @@ function toggleParentVisibility(query: string, action: 'show' | 'hide') {
function handleScopeChange
(view: P, element: HTMLSelectElement) { // Scope type's innerText looks something like `DCIM > region`. const scopeType = element.options[element.selectedIndex].innerText.toLowerCase(); + const layoutConfig = showHideMap[view]; - for (const [scope, fields] of Object.entries(showHideMap[view])) { + for (const [scope, fields] of Object.entries(showHideLayout[layoutConfig])) { // If the scope type ends with the specified scope, toggle its field visibility according to // the show/hide values. if (scopeType.endsWith(scope)) { @@ -109,7 +130,7 @@ function handleScopeChange
(view: P, element: HTMLSe break; } else { // Otherwise, hide all fields. - for (const field of showHideMap[view].default.hide) { + for (const field of showHideLayout[layoutConfig].default.hide) { toggleParentVisibility(`#${field}`, 'hide'); } } diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss index 2db29ad38..c0933e991 100644 --- a/netbox/project-static/styles/theme-dark.scss +++ b/netbox/project-static/styles/theme-dark.scss @@ -23,7 +23,6 @@ $theme-colors: ( 'danger': $danger, 'light': $light, 'dark': $dark, - // General-purpose palette 'blue': $blue-300, 'indigo': $indigo-300, @@ -37,7 +36,7 @@ $theme-colors: ( 'cyan': $cyan-300, 'gray': $gray-300, 'black': $black, - 'white': $white, + 'white': $white ); // Gradient @@ -146,9 +145,9 @@ $nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg $nav-pills-link-active-color: $component-active-color; $nav-pills-link-active-bg: $component-active-bg; -$navbar-light-color: $darkest; -$navbar-light-toggler-icon-bg: url("data:image/svg+xml,"); +$navbar-light-color: $darker; $navbar-light-toggler-border-color: $gray-700; +$navbar-light-toggler-icon-bg: url("data:image/svg+xml,"); // Dropdowns $dropdown-color: $body-color; diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index 6e71b3995..08ab33eff 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -139,7 +139,7 @@
{# Page layout #} diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index cf3841dd2..da2d10c65 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -108,56 +108,58 @@ {# Page footer #}